27 Commits
v0.7.0 ... main

Author SHA1 Message Date
Andrew Miller
424732cc91 docs: update README to use gitea-mcp-extended repo name
All references to the old gitea-mcp repo name have been updated to
gitea-mcp-extended, including URLs, binary names, and go module paths.
2026-03-05 14:10:58 -05:00
Andrew Miller
fd4a4db89c fix: simplify release workflow to goreleaser only, remove nightly/package publishing
All checks were successful
release / goreleaser (push) Successful in 1m5s
2026-03-05 13:43:35 -05:00
Andrew Miller
b94da1384b debug: check if org secret is being injected
Some checks failed
release-nightly / publish-nightly (push) Failing after 34s
2026-03-05 13:35:51 -05:00
Andrew Miller
69e4aa00a6 fix: use org secret DEPLOYER_API_GITEA_API_KEY for package uploads
Some checks failed
release-nightly / publish-nightly (push) Failing after 45s
release / goreleaser (push) Successful in 1m14s
release / publish-packages (push) Failing after 35s
2026-03-05 13:32:35 -05:00
Andrew Miller
3d83c18cdf fix: add packages:write permission to release workflows
Some checks failed
release-nightly / publish-nightly (push) Failing after 49s
release / goreleaser (push) Successful in 1m17s
release / publish-packages (push) Failing after 33s
The automatic GITHUB_TOKEN needs explicit package scope for
uploading to the Gitea Generic Package Registry.
2026-03-05 13:28:01 -05:00
Andrew Miller
41adbc460b fix: use token auth header for package registry uploads
Some checks failed
release-nightly / publish-nightly (push) Failing after 48s
release / goreleaser (push) Successful in 1m15s
release / publish-packages (push) Failing after 35s
The GITHUB_TOKEN with basic auth returns 401 reqPackageAccess.
Switch to Authorization: token header which grants proper scope.
2026-03-05 13:23:50 -05:00
Andrew Miller
1b806fc393 fix: improve package publish debugging and hardcode API URL
Some checks failed
release-nightly / publish-nightly (push) Failing after 51s
release / goreleaser (push) Successful in 1m17s
release / publish-packages (push) Failing after 33s
- Hardcode git.lethalbits.com instead of gitea.server_url variable
- Replace brace expansion with separate glob patterns for POSIX compat
- Add HTTP status code checking with response body on failure
- Add debug output for URL, actor, and file listing
2026-03-05 13:20:15 -05:00
Andrew Miller
d3c4e271ef fix: resolve CI failures in release workflows
Some checks failed
release-nightly / publish-nightly (push) Failing after 46s
release / goreleaser (push) Successful in 1m15s
release / publish-packages (push) Failing after 33s
- Downgrade setup-go@v6 to v5 (v6 breaks with "version: not found" on Gitea runner)
- Use go-version-file instead of go-version: stable for consistency
- Add skip_tls_verify to goreleaser gitea_urls for Cloudflare origin cert
- Add curl -k flag for package registry uploads
2026-03-05 13:11:51 -05:00
Andrew Miller
9b407fb70a fix: add GIT_SSL_NO_VERIFY for Cloudflare origin cert in CI
Some checks failed
release-nightly / publish-nightly (push) Failing after 8s
release / goreleaser (push) Failing after 1m12s
release / publish-packages (push) Has been skipped
The runner containers don't trust the Cloudflare origin certificate
for git.lethalbits.com, causing checkout to fail. Set GIT_SSL_NO_VERIFY
and Go private module env vars at the workflow level for all workflows.
2026-03-05 13:06:37 -05:00
Andrew Miller
60f05e5f1e feat: rename to gitea-mcp-extended and add package publishing workflows
Some checks failed
release-nightly / publish-nightly (push) Failing after 44s
release / goreleaser (push) Failing after 37s
release / publish-packages (push) Has been skipped
Rename the fork from gitea-mcp to gitea-mcp-extended to reflect the
significantly expanded tool coverage (299 vs upstream's 93 tools).

- Rename Go module path and all import references
- Rename binary to gitea-mcp-extended in Makefile, Dockerfile, .gitignore
- Point .goreleaser.yaml gitea_urls to git.lethalbits.com
- Replace release-tag workflow with goreleaser + Generic Package Registry publishing
- Replace release-nightly workflow with cross-platform build + nightly package publishing
- Update CLAUDE.md project description and tool count
2026-03-05 13:04:02 -05:00
Andrew Miller
df25d328d1 feat: expand MCP tool coverage from 93 to 299 tools
Some checks failed
release-nightly / release-image (push) Failing after 2m17s
Fork the official gitea-mcp and massively extend API coverage:

- Organization: orgs, members, teams, hooks, blocks, activity (34 tools)
- User: profile, followers, keys, emails, repos, blocks (30 tools)
- Repository: collaborators, webhooks, branch/tag protection, deploy keys,
  topics, git refs/trees/notes, commit status, stars/watchers, forks,
  transfers, mirrors, templates (53 tools)
- Issue: reactions, pins, subscriptions, timeline, templates (16 tools)
- Notifications: list, check, read, repo-scoped (7 tools)
- Settings: API, attachment, repo, UI settings (4 tools)
- Packages: list, get, delete, files, latest, link/unlink (7 tools)
- Miscellaneous: server version, gitignore/label/license templates,
  markdown/markup rendering, node info, signing keys (12 tools)
- Admin: user/org/repo CRUD, system webhooks, cron tasks, unadopted
  repos, emails, badges (23 tools)

Module path updated to git.lethalbits.com/lethalbits/gitea-mcp.
2026-03-05 11:03:20 -05:00
silverwind
9ce5604e4c Improve CLI help text and flags (#139)
## Summary
- Replace default `flag.Usage` with custom 2-column layout using `text/tabwriter`
- Add short and long aliases for all CLI flags
- Add environment variables section with sorted entries
- Handle `-version` flag in `Execute()`

## Sample output
```
Usage: gitea-mcp [options]

Options:
  -t, -transport <type>   Transport type: stdio or http (default: stdio)
  -H, -host <url>         Gitea host URL (default: https://gitea.com)
  -p, -port <number>      HTTP server port (default: 8080)
  -T, -token <token>      Personal access token
  -r, -read-only          Expose only read-only tools
  -d, -debug              Enable debug mode
  -k, -insecure           Ignore TLS certificate errors
  -v, -version            Print version and exit

Environment variables:
  GITEA_ACCESS_TOKEN   Provide access token
  GITEA_DEBUG          Set to 'true' for debug mode
  GITEA_HOST           Override Gitea host URL
  GITEA_INSECURE       Set to 'true' to ignore TLS errors
  GITEA_READONLY       Set to 'true' for read-only mode
  MCP_MODE             Override transport mode
```

*Created by Claude on behalf of @silverwind*

Reviewed-on: https://gitea.com/gitea/gitea-mcp/pulls/139
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-committed-by: silverwind <me@silverwind.io>
2026-02-26 18:49:37 +00:00
silverwind
653781a199 fix: replace goreleaser-action with direct go install
The goreleaser/goreleaser-action@v7 JS wrapper was crashing in CI
before goreleaser could run. Install goreleaser via `go install`
instead to avoid the action's compatibility issues with the runner.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 02:09:18 +01:00
silverwind
67a1e1e7fe feat: accept string values for all numeric input parameters (#138)
## Summary

- MCP clients may send numbers as strings. This adds `ToInt64` and `GetOptionalInt` helpers to `pkg/params` and replaces all raw `.(float64)` type assertions across operation handlers to accept both `float64` and string inputs.

## Test plan

- [x] Verify `go test ./...` passes
- [x] Test with an MCP client that sends numeric parameters as strings

*Created by Claude on behalf of @silverwind*

Reviewed-on: https://gitea.com/gitea/gitea-mcp/pulls/138
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-committed-by: silverwind <me@silverwind.io>
2026-02-25 23:28:14 +00:00
silverwind
4a2935d898 feat: add title parameter to merge_pull_request tool (#134)
## Summary
- Add missing `title` parameter to `merge_pull_request` tool for custom merge commit titles
- Use `params.GetIndex()` for consistent index parameter handling (supports both string and number inputs)
- Add test for `MergePullRequestFn`

Closes #120

## Test plan
- [x] `go test ./operation/pull/ -run TestMergePullRequestFn` passes
- [x] All existing tests pass (`go test ./...`)
- [x] Build succeeds (`make build`)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/gitea-mcp/pulls/134
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: silverwind <silverwind@noreply.gitea.com>
Co-committed-by: silverwind <silverwind@noreply.gitea.com>
2026-02-25 19:05:09 +00:00
silverwind
6540693771 fix: parse Authorization header case-insensitively and support token format (#137)
## Summary
- Make auth header parsing RFC 7235 compliant by comparing the scheme case-insensitively (`bearer`, `BEARER`, etc. all work now)
- Add support for Gitea-style `token <value>` format in addition to `Bearer <value>`

Fixes https://gitea.com/gitea/gitea-mcp/issues/59

---
*This PR was authored by Claude.*

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/gitea-mcp/pulls/137
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-committed-by: silverwind <me@silverwind.io>
2026-02-25 19:04:14 +00:00
silverwind
3b9236695c fix: add missing required attributes to search tool schemas (#135)
## Summary
- Add `mcp.Required()` to `keyword` in `search_repos` and `search_users` tool schemas
- Add `mcp.Required()` to `org` and `query` in `search_org_teams` tool schema
- Add test verifying required fields are set on all search tool schemas
- Fixes MCP clients failing with `keyword is required` because the schema didn't declare the field as required

Closes #115

---
*This PR was authored by Claude.*

Reviewed-on: https://gitea.com/gitea/gitea-mcp/pulls/135
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: silverwind <silverwind@noreply.gitea.com>
Co-committed-by: silverwind <silverwind@noreply.gitea.com>
2026-02-25 19:00:17 +00:00
silverwind
723a30ae23 fix: replace deprecated SDK calls in timetracking (#136)
## Summary
- Replace `client.GetMyStopwatches()` with `client.ListMyStopwatches(ListStopwatchesOptions{})`
- Replace `client.GetMyTrackedTimes()` with `client.ListMyTrackedTimes(ListTrackedTimesOptions{})`
- Fixes staticcheck SA1019 lint errors for deprecated API usage

## Test plan
- [x] `make lint` passes with 0 issues
- [x] `go test ./...` passes

*Created by Claude on behalf of @silverwind*

Reviewed-on: https://gitea.com/gitea/gitea-mcp/pulls/136
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-committed-by: silverwind <me@silverwind.io>
2026-02-25 03:28:59 +00:00
techknowlogick
7bbe015ea7 Bump to go 1.26 2026-02-22 17:13:24 +00:00
silverwind
bb9470a259 chore: update Go and Actions dependencies (#132)
## Summary
- Update Go dependencies: gitea SDK v0.22.1→v0.23.2, mcp-go v0.42.0→v0.44.0, zap v1.27.0→v1.27.1, go-version v1.7.0→v1.8.0, x/crypto v0.43.0→v0.48.0, x/sys v0.37.0→v0.41.0
- Update Actions: checkout v4→v6, setup-go v5→v6, build-push-action v5→v6, goreleaser-action v6→v7

## Test plan
- [x] `go test ./...` passes
- [x] `make build` succeeds

Reviewed-on: https://gitea.com/gitea/gitea-mcp/pulls/132
Co-authored-by: silverwind <me@silverwind.io>
Co-committed-by: silverwind <me@silverwind.io>
2026-02-22 17:10:25 +00:00
silverwind
8728c04748 chore: add golangci-lint, bump Go to 1.26, fix all lint issues (#133)
## Summary
- Add `.golangci.yml` with linter configuration matching the main gitea repo
- Add `lint`, `lint-fix`, `lint-go`, `lint-go-fix`, and `security-check` Makefile targets
- Add `tidy` Makefile target (extracts min Go version from `go.mod` for `-compat` flag)
- Bump minimum Go version to 1.26
- Update golangci-lint to v2.10.1
- Replace `golang/govulncheck-action` with `make security-check` in CI
- Add `make lint` step to CI
- Fix all lint issues across the codebase (formatting, `errors.New` vs `fmt.Errorf`, `any` vs `interface{}`, unused returns, stuttering names, Go 1.26 `new(expr)`, etc.)
- Remove unused `pkg/ptr` package (inlined by Go 1.26 `new(expr)`)
- Remove dead linter exclusions (staticcheck, gocritic, testifylint, dupl)

## Test plan
- [x] `make lint` passes
- [x] `go test ./...` passes
- [x] `make build` succeeds

Reviewed-on: https://gitea.com/gitea/gitea-mcp/pulls/133
Reviewed-by: techknowlogick <techknowlogick@noreply.gitea.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-committed-by: silverwind <me@silverwind.io>
2026-02-22 17:10:04 +00:00
silverwind
4d5fa3ab2c feat: accept string or number for index parameters (#131)
This change makes index parameters more flexible by accepting both numeric and string values. LLM agents often pass issue/PR indices as strings (e.g., "123") since they appear as string identifiers in URLs and CLI contexts. The implementation:

- Created `pkg/params` package with `GetIndex()` helper function
- Updated 25+ tool functions across issue, pull, label, and timetracking operations
- Improved error messages to say "must be a valid integer" instead of misleading "is required"
- Added comprehensive tests for both numeric and string inputs

Based on #122 by @jamespharaoh with review feedback applied (replaced custom `contains()` test helper with `strings.Contains`). Verified working in Claude Code.

Fixes: https://gitea.com/gitea/gitea-mcp/issues/121
Fixes: https://gitea.com/gitea/gitea-mcp/issues/122
---------

Co-authored-by: James Pharaoh <james@pharaoh.uk>
Reviewed-on: https://gitea.com/gitea/gitea-mcp/pulls/131
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-committed-by: silverwind <me@silverwind.io>
2026-02-20 23:47:22 +00:00
silverwind
21e4e1b42b feat: add edit_pull_request tool (#125)
## Summary
- Add `edit_pull_request` MCP tool to modify pull request properties
- Supports editing title, body, base branch, assignees, milestone, state, and maintainer edit permission
- Enables toggling WIP/draft status by modifying the title prefix

Fixes https://gitea.com/gitea/gitea-mcp/issues/124

## Test plan
- [x] `go test ./...` passes
- [x] Verified against gitea.com: toggled WIP on/off via title edit, changed PR state

🤖 Generated with [Claude Code](https://claude.ai/claude-code)

Reviewed-on: https://gitea.com/gitea/gitea-mcp/pulls/125
Reviewed-by: Bo-Yi Wu (吳柏毅) <appleboy.tw@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-committed-by: silverwind <me@silverwind.io>
2026-02-13 13:26:21 +00:00
tylermitchell
4aacfe348a feat(pull): add merge_pull_request tool (#123)
Add MCP tool to merge pull requests with support for:
- Multiple merge styles (merge, rebase, rebase-merge, squash, fast-forward-only)
- Custom merge commit messages
- Optional branch deletion after merge
- Detailed error handling for merge conflicts and edge cases

Updated all README files (English, Simplified Chinese, Traditional Chinese)
with the new tool entry.

---------

Co-authored-by: Tyler Potts <tyler@adhdafterdiagnosis.com>
Reviewed-on: https://gitea.com/gitea/gitea-mcp/pulls/123
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: silverwind <silverwind@noreply.gitea.com>
Co-authored-by: tylermitchell <tylermitchell@noreply.gitea.com>
Co-committed-by: tylermitchell <tylermitchell@noreply.gitea.com>
2026-02-11 17:53:09 +00:00
silverwind
1f7392305f docs: add --scope user to Claude Code examples in READMEs (#118)
Small followup to https://gitea.com/gitea/gitea-mcp/pulls/117. By using the user scope, the MCP server connection will be saved into the user's home directory, making it available for all repos, which is more useful than having to do this per-repo.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------

Co-authored-by: hiifong <f@f.style>
Reviewed-on: https://gitea.com/gitea/gitea-mcp/pulls/118
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: hiifong <f@f.style>
Co-authored-by: silverwind <me@silverwind.io>
Co-committed-by: silverwind <me@silverwind.io>
2026-02-06 03:16:37 +00:00
Gustav
c3b24d65fe added support for get_pull_request_diff (#119)
This function call is needed to be able to do AI code review to actually get the diff from the PR.

Reviewed-on: https://gitea.com/gitea/gitea-mcp/pulls/119
Reviewed-by: silverwind <silverwind@noreply.gitea.com>
Reviewed-by: hiifong <f@f.style>
Co-authored-by: Gustav <tvarsis@hotmail.com>
Co-committed-by: Gustav <tvarsis@hotmail.com>
2026-02-06 03:14:47 +00:00
silverwind
dcd01441c5 docs: add Claude Code usage example to README files (#117)
I verified this is working:

```bash
$ claude mcp list | grep gitea
gitea: go run gitea.com/gitea/gitea-mcp@latest -t stdio - ✓ Connected
```

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Reviewed-on: https://gitea.com/gitea/gitea-mcp/pulls/117
Reviewed-by: hiifong <f@f.style>
Co-authored-by: silverwind <me@silverwind.io>
Co-committed-by: silverwind <me@silverwind.io>
2026-02-04 02:45:37 +00:00
83 changed files with 9095 additions and 1373 deletions

View File

@@ -1,51 +0,0 @@
name: release-nightly
on:
push:
branches: [main]
tags:
- "*"
jobs:
release-image:
runs-on: ubuntu-latest
env:
DOCKER_ORG: gitea
DOCKER_LATEST: nightly
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # all history for all branches and tags
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker BuildX
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Get Meta
id: meta
run: |
echo REPO_NAME=$(echo ${GITHUB_REPOSITORY} | awk -F"/" '{print $2}') >> $GITHUB_OUTPUT
echo REPO_VERSION=$(git describe --tags --always | sed 's/-/+/' | sed 's/^v//') >> $GITHUB_OUTPUT
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
platforms: |
linux/amd64
linux/arm64
push: true
tags: |
${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}-server:${{ env.DOCKER_LATEST }}
build-args: |
VERSION=${{ steps.meta.outputs.REPO_VERSION }}

View File

@@ -2,72 +2,29 @@ name: release
on: on:
push: push:
tags: tags: ["*"]
- "*"
env:
GIT_SSL_NO_VERIFY: true
GOPRIVATE: git.lethalbits.com/*
GONOSUMCHECK: git.lethalbits.com/*
GOINSECURE: git.lethalbits.com/*
permissions:
contents: write
jobs: jobs:
goreleaser: goreleaser:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - uses: actions/checkout@v6
uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Go - uses: actions/setup-go@v5
uses: actions/setup-go@v5
with: with:
go-version: stable go-version-file: 'go.mod'
- name: Run GoReleaser - run: go install github.com/goreleaser/goreleaser/v2@latest
uses: goreleaser/goreleaser-action@v6 - run: goreleaser release --clean
with:
distribution: goreleaser
# 'latest', 'nightly', or a semver
version: "~> v2"
args: release --clean
env: env:
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GORELEASER_FORCE_TOKEN: "gitea" GORELEASER_FORCE_TOKEN: "gitea"
release-image:
runs-on: ubuntu-latest
env:
DOCKER_ORG: gitea
DOCKER_LATEST: latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # all history for all branches and tags
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker BuildX
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Get Meta
id: meta
run: |
echo REPO_NAME=$(echo ${GITHUB_REPOSITORY} | awk -F"/" '{print $2}') >> $GITHUB_OUTPUT
echo REPO_VERSION=${GITHUB_REF_NAME#v} >> $GITHUB_OUTPUT
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
platforms: |
linux/amd64
linux/arm64
push: true
build-args: |
VERSION=${{ steps.meta.outputs.REPO_VERSION }}
tags: |
${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}-server:${{ steps.meta.outputs.REPO_VERSION }}
${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}-server:${{ env.DOCKER_LATEST }}

View File

@@ -3,24 +3,23 @@ name: check-and-test
on: on:
- pull_request - pull_request
env:
GIT_SSL_NO_VERIFY: true
GOPRIVATE: git.lethalbits.com/*
GONOSUMCHECK: git.lethalbits.com/*
GOINSECURE: git.lethalbits.com/*
jobs: jobs:
check-and-test: check-and-test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
- uses: actions/setup-go@v5 - uses: actions/setup-go@v6
with: with:
go-version-file: 'go.mod' go-version-file: 'go.mod'
- name: lint
run: make lint
- name: build - name: build
run: | run: make build
make build - name: security-check
run: make security-check
govulncheck_job:
runs-on: ubuntu-latest
name: Run govulncheck
steps:
- id: govulncheck
uses: golang/govulncheck-action@v1
with:
go-version-file: 'go.mod'
go-package: ./...

4
.gitignore vendored
View File

@@ -1,5 +1,5 @@
.idea .idea
gitea-mcp gitea-mcp-extended
gitea-mcp.exe gitea-mcp-extended.exe
*.log *.log
tmp tmp

113
.golangci.yml Normal file
View File

@@ -0,0 +1,113 @@
version: "2"
output:
sort-order:
- file
linters:
default: none
enable:
- bidichk
- bodyclose
- depguard
- errcheck
- forbidigo
- gocheckcompilerdirectives
- gocritic
- govet
- ineffassign
- mirror
- modernize
- nakedret
- nilnil
- nolintlint
- perfsprint
- revive
- staticcheck
- testifylint
- unconvert
- unparam
- unused
- usestdlibvars
- usetesting
- wastedassign
settings:
depguard:
rules:
main:
deny:
- pkg: io/ioutil
desc: use os or io instead
- pkg: golang.org/x/exp
desc: it's experimental and unreliable
- pkg: github.com/pkg/errors
desc: use builtin errors package instead
nolintlint:
allow-unused: false
require-explanation: true
require-specific: true
gocritic:
enabled-checks:
- equalFold
disabled-checks: []
revive:
severity: error
rules:
- name: blank-imports
- name: constant-logical-expr
- name: context-as-argument
- name: context-keys-type
- name: dot-imports
- name: empty-lines
- name: error-return
- name: error-strings
- name: exported
- name: identical-branches
- name: if-return
- name: increment-decrement
- name: modifies-value-receiver
- name: package-comments
- name: redefines-builtin-id
- name: superfluous-else
- name: time-naming
- name: unexported-return
- name: var-declaration
- name: var-naming
disabled: true
staticcheck:
checks:
- all
testifylint: {}
usetesting:
os-temp-dir: true
perfsprint:
concat-loop: false
govet:
enable:
- nilness
- unusedwrite
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
rules:
- linters:
- errcheck
- staticcheck
- unparam
path: _test\.go
issues:
max-issues-per-linter: 0
max-same-issues: 0
formatters:
enable:
- gofmt
- gofumpt
settings:
gofumpt:
extra-rules: true
exclusions:
generated: lax
run:
timeout: 10m

View File

@@ -71,6 +71,7 @@ release:
Released by [GoReleaser](https://github.com/goreleaser/goreleaser). Released by [GoReleaser](https://github.com/goreleaser/goreleaser).
gitea_urls: gitea_urls:
api: https://gitea.com/api/v1 api: https://git.lethalbits.com/api/v1
download: https://gitea.com download: https://git.lethalbits.com
skip_tls_verify: true
force_token: gitea force_token: gitea

71
AGENTS.md Normal file
View File

@@ -0,0 +1,71 @@
# AGENTS.md
This file provides guidance to AI coding agents when working with code in this repository.
## Development Commands
**Build**: `make build` - Build the gitea-mcp binary
**Install**: `make install` - Build and install to GOPATH/bin
**Clean**: `make clean` - Remove build artifacts
**Test**: `go test ./...` - Run all tests
**Hot reload**: `make dev` - Start development server with hot reload (requires air)
**Dependencies**: `make vendor` - Tidy and verify module dependencies
## Architecture Overview
This is a **Gitea MCP (Model Context Protocol) Server** written in Go that provides MCP tools for interacting with Gitea repositories, issues, pull requests, users, and more.
**Core Components**:
- `main.go` + `cmd/cmd.go`: CLI entry point and flag parsing
- `operation/operation.go`: Main server setup and tool registration
- `pkg/tool/tool.go`: Tool registry with read/write categorization
- `operation/*/`: Individual tool modules (user, repo, issue, pull, search, wiki, etc.)
**Transport Modes**:
- **stdio** (default): Standard input/output for MCP clients
- **http**: HTTP server mode on configurable port (default 8080)
**Authentication**:
- Global token via `--token` flag or `GITEA_ACCESS_TOKEN` env var
- HTTP mode supports per-request Bearer token override in Authorization header
- Token precedence: HTTP Authorization header > CLI flag > environment variable
**Tool Organization**:
- Tools are categorized as read-only or write operations
- `--read-only` flag exposes only read tools
- Tool modules register via `Tool.RegisterRead()` and `Tool.RegisterWrite()`
**Key Configuration**:
- Default Gitea host: `https://gitea.com` (override with `--host` or `GITEA_HOST`)
- Environment variables can override CLI flags: `MCP_MODE`, `GITEA_READONLY`, `GITEA_DEBUG`, `GITEA_INSECURE`
- Logs are written to `~/.gitea-mcp/gitea-mcp.log` with rotation
## Available Tools
The server provides 40+ MCP tools covering:
- **User**: get_my_user_info, get_user_orgs, search_users
- **Repository**: create_repo, fork_repo, list_my_repos, search_repos
- **Branches/Tags**: create_branch, delete_branch, list_branches, create_tag, list_tags
- **Files**: get_file_content, create_file, update_file, delete_file, get_dir_content
- **Issues**: create_issue, list_repo_issues, create_issue_comment, edit_issue
- **Pull Requests**: create_pull_request, list_repo_pull_requests, get_pull_request_by_index
- **Releases**: create_release, list_releases, get_latest_release
- **Wiki**: create_wiki_page, update_wiki_page, list_wiki_pages
- **Search**: search_repos, search_users, search_org_teams
- **Version**: get_gitea_mcp_server_version
## Common Development Patterns
**Testing**: Use `go test ./operation -run TestFunctionName` for specific tests
**Token Context**: HTTP requests use `pkg/context.TokenContextKey` for request-scoped token access
**Flag Access**: All packages access configuration via global variables in `pkg/flag/flag.go`
**Graceful Shutdown**: HTTP mode implements graceful shutdown with 10-second timeout on SIGTERM/SIGINT

View File

@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Development Commands ## Development Commands
**Build**: `make build` - Build the gitea-mcp binary **Build**: `make build` - Build the gitea-mcp-extended binary
**Install**: `make install` - Build and install to GOPATH/bin **Install**: `make install` - Build and install to GOPATH/bin
**Clean**: `make clean` - Remove build artifacts **Clean**: `make clean` - Remove build artifacts
**Test**: `go test ./...` - Run all tests **Test**: `go test ./...` - Run all tests
@@ -13,7 +13,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Architecture Overview ## Architecture Overview
This is a **Gitea MCP (Model Context Protocol) Server** written in Go that provides MCP tools for interacting with Gitea repositories, issues, pull requests, users, and more. This is **gitea-mcp-extended**, a Gitea MCP (Model Context Protocol) Server written in Go that provides 299+ MCP tools for interacting with Gitea repositories, issues, pull requests, users, and more. It is a fork of [gitea/gitea-mcp](https://gitea.com/gitea/gitea-mcp) with significantly expanded tool coverage.
**Core Components**: **Core Components**:
@@ -43,11 +43,11 @@ This is a **Gitea MCP (Model Context Protocol) Server** written in Go that provi
- Default Gitea host: `https://gitea.com` (override with `--host` or `GITEA_HOST`) - Default Gitea host: `https://gitea.com` (override with `--host` or `GITEA_HOST`)
- Environment variables can override CLI flags: `MCP_MODE`, `GITEA_READONLY`, `GITEA_DEBUG`, `GITEA_INSECURE` - Environment variables can override CLI flags: `MCP_MODE`, `GITEA_READONLY`, `GITEA_DEBUG`, `GITEA_INSECURE`
- Logs are written to `~/.gitea-mcp/gitea-mcp.log` with rotation - Logs are written to `~/.gitea-mcp/gitea-mcp.log` with rotation (log dir unchanged for backward compatibility)
## Available Tools ## Available Tools
The server provides 40+ MCP tools covering: The server provides 299+ MCP tools covering:
- **User**: get_my_user_info, get_user_orgs, search_users - **User**: get_my_user_info, get_user_orgs, search_users
- **Repository**: create_repo, fork_repo, list_my_repos, search_repos - **Repository**: create_repo, fork_repo, list_my_repos, search_repos

View File

@@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1.4 # syntax=docker/dockerfile:1.4
# Build stage # Build stage
FROM --platform=$BUILDPLATFORM golang:1.24-alpine AS builder FROM --platform=$BUILDPLATFORM golang:1.26-alpine AS builder
ARG VERSION=dev ARG VERSION=dev
ARG TARGETOS ARG TARGETOS
@@ -17,16 +17,16 @@ COPY . .
RUN --mount=type=cache,target=/go/pkg/mod \ RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \ --mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH:-amd64} \ CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH:-amd64} \
go build -trimpath -ldflags="-s -w -X main.Version=${VERSION}" -o gitea-mcp go build -trimpath -ldflags="-s -w -X main.Version=${VERSION}" -o gitea-mcp-extended
# Final stage # Final stage
FROM gcr.io/distroless/static-debian12:nonroot FROM gcr.io/distroless/static-debian12:nonroot
WORKDIR /app WORKDIR /app
COPY --from=builder --chown=nonroot:nonroot /app/gitea-mcp . COPY --from=builder --chown=nonroot:nonroot /app/gitea-mcp-extended .
USER nonroot:nonroot USER nonroot:nonroot
LABEL org.opencontainers.image.version="${VERSION}" LABEL org.opencontainers.image.version="${VERSION}"
CMD ["/app/gitea-mcp"] CMD ["/app/gitea-mcp-extended"]

View File

@@ -1,8 +1,12 @@
GO ?= go GO ?= go
EXECUTABLE := gitea-mcp EXECUTABLE := gitea-mcp-extended
VERSION ?= $(shell git describe --tags --always | sed 's/-/+/' | sed 's/^v//') VERSION ?= $(shell git describe --tags --always | sed 's/-/+/' | sed 's/^v//')
LDFLAGS := -X "main.Version=$(VERSION)" LDFLAGS := -X "main.Version=$(VERSION)"
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.10.1
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1
GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.9.2
.PHONY: help .PHONY: help
help: ## Print this help message. help: ## Print this help message.
@echo "Usage: make [target]" @echo "Usage: make [target]"
@@ -43,10 +47,31 @@ air: ## Install air for hot reload.
.PHONY: dev .PHONY: dev
dev: air ## run the application with hot reload dev: air ## run the application with hot reload
air --build.cmd "make build" --build.bin ./gitea-mcp air --build.cmd "make build" --build.bin ./gitea-mcp-extended
.PHONY: lint
lint: lint-go ## lint everything
.PHONY: lint-fix
lint-fix: lint-go-fix ## lint everything and fix issues
.PHONY: lint-go
lint-go: ## lint go files
$(GO) run $(GOLANGCI_LINT_PACKAGE) run
.PHONY: lint-go-fix
lint-go-fix: ## lint go files and fix issues
$(GO) run $(GOLANGCI_LINT_PACKAGE) run --fix
.PHONY: security-check
security-check: ## run security check
$(GO) run $(GOVULNCHECK_PACKAGE) -show color ./... || true
.PHONY: tidy
tidy: ## run go mod tidy
$(eval MIN_GO_VERSION := $(shell grep -Eo '^go\s+[0-9]+\.[0-9.]+' go.mod | cut -d' ' -f2))
$(GO) mod tidy -compat=$(MIN_GO_VERSION)
.PHONY: vendor .PHONY: vendor
vendor: ## tidy and verify module dependencies vendor: tidy ## tidy and verify module dependencies
@echo 'Tidying and verifying module dependencies...' $(GO) mod verify
go mod tidy
go mod verify

View File

@@ -1,18 +1,15 @@
# Gitea MCP Server # Gitea MCP Extended Server
[繁體中文](README.zh-tw.md) | [简体中文](README.zh-cn.md) **Gitea MCP Extended Server** is a fork of [gitea/gitea-mcp](https://gitea.com/gitea/gitea-mcp) with significantly expanded tool coverage (299+ tools). It connects Gitea with Model Context Protocol (MCP) systems, allowing seamless command execution and repository management through an MCP-compatible chat interface.
**Gitea MCP Server** is an integration plugin designed to connect Gitea with Model Context Protocol (MCP) systems. This allows for seamless command execution and repository management through an MCP-compatible chat interface.
[![Install with Docker in VS Code](https://img.shields.io/badge/VS_Code-Install_Server-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=gitea&inputs=[{%22id%22:%22gitea_token%22,%22type%22:%22promptString%22,%22description%22:%22Gitea%20Personal%20Access%20Token%22,%22password%22:true}]&config={%22command%22:%22docker%22,%22args%22:[%22run%22,%22-i%22,%22--rm%22,%22-e%22,%22GITEA_ACCESS_TOKEN%22,%22docker.gitea.com/gitea-mcp-server%22],%22env%22:{%22GITEA_ACCESS_TOKEN%22:%22${input:gitea_token}%22}}) [![Install with Docker in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install_Server-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=gitea&inputs=[{%22id%22:%22gitea_token%22,%22type%22:%22promptString%22,%22description%22:%22Gitea%20Personal%20Access%20Token%22,%22password%22:true}]&config={%22command%22:%22docker%22,%22args%22:[%22run%22,%22-i%22,%22--rm%22,%22-e%22,%22GITEA_ACCESS_TOKEN%22,%22docker.gitea.com/gitea-mcp-server%22],%22env%22:{%22GITEA_ACCESS_TOKEN%22:%22${input:gitea_token}%22}}&quality=insiders)
## Table of Contents ## Table of Contents
- [Gitea MCP Server](#gitea-mcp-server) - [Gitea MCP Extended Server](#gitea-mcp-extended-server)
- [Table of Contents](#table-of-contents) - [Table of Contents](#table-of-contents)
- [What is Gitea?](#what-is-gitea) - [What is Gitea?](#what-is-gitea)
- [What is MCP?](#what-is-mcp) - [What is MCP?](#what-is-mcp)
- [🚧 Installation](#-installation) - [🚧 Installation](#-installation)
- [Usage with Claude Code](#usage-with-claude-code)
- [Usage with VS Code](#usage-with-vs-code) - [Usage with VS Code](#usage-with-vs-code)
- [📥 Download the official binary release](#-download-the-official-binary-release) - [📥 Download the official binary release](#-download-the-official-binary-release)
- [🔧 Build from Source](#-build-from-source) - [🔧 Build from Source](#-build-from-source)
@@ -32,6 +29,17 @@ Model Context Protocol (MCP) is a protocol that allows for the integration of va
## 🚧 Installation ## 🚧 Installation
### Usage with Claude Code
This method uses `go run` and requires [Go](https://go.dev) to be installed.
```bash
claude mcp add --transport stdio --scope user gitea \
--env GITEA_ACCESS_TOKEN=token \
--env GITEA_HOST=https://gitea.com \
-- go run git.lethalbits.com/lethalbits/gitea-mcp-extended@latest -t stdio
```
### Usage with VS Code ### Usage with VS Code
For quick installation, use one of the one-click install buttons at the top of this README. For quick installation, use one of the one-click install buttons at the top of this README.
@@ -75,14 +83,14 @@ Optionally, you can add it to a file called `.vscode/mcp.json` in your workspace
### 📥 Download the official binary release ### 📥 Download the official binary release
You can download the official release from [official Gitea MCP binary releases](https://gitea.com/gitea/gitea-mcp/releases). You can download the official release from [official Gitea MCP Extended binary releases](https://git.lethalbits.com/lethalbits/gitea-mcp-extended/releases).
### 🔧 Build from Source ### 🔧 Build from Source
You can download the source code by cloning the repository using Git: You can download the source code by cloning the repository using Git:
```bash ```bash
git clone https://gitea.com/gitea/gitea-mcp.git git clone https://git.lethalbits.com/lethalbits/gitea-mcp-extended.git
``` ```
Before building, make sure you have the following installed: Before building, make sure you have the following installed:
@@ -98,10 +106,10 @@ make install
### 📁 Add to PATH ### 📁 Add to PATH
After installing, copy the binary gitea-mcp to a directory included in your system's PATH. For example: After installing, copy the binary gitea-mcp-extended to a directory included in your system's PATH. For example:
```bash ```bash
cp gitea-mcp /usr/local/bin/ cp gitea-mcp-extended /usr/local/bin/
``` ```
## 🚀 Usage ## 🚀 Usage
@@ -115,7 +123,7 @@ To configure the MCP server for Gitea, add the following to your MCP configurati
{ {
"mcpServers": { "mcpServers": {
"gitea": { "gitea": {
"command": "gitea-mcp", "command": "gitea-mcp-extended",
"args": [ "args": [
"-t", "-t",
"stdio", "stdio",
@@ -148,7 +156,7 @@ To configure the MCP server for Gitea, add the following to your MCP configurati
} }
``` ```
**Default log path**: `$HOME/.gitea-mcp/gitea-mcp.log` **Default log path**: `$HOME/.gitea-mcp/gitea-mcp.log` (log directory unchanged from upstream for backward compatibility)
> [!NOTE] > [!NOTE]
> You can provide your Gitea host and access token either as command-line arguments or environment variables. > You can provide your Gitea host and access token either as command-line arguments or environment variables.
@@ -162,7 +170,7 @@ list all my repositories
## ✅ Available Tools ## ✅ Available Tools
The Gitea MCP Server supports the following tools: The Gitea MCP Extended Server supports the following tools:
| Tool | Scope | Description | | Tool | Scope | Description |
| :-------------------------------: | :----------: | :------------------------------------------------------: | | :-------------------------------: | :----------: | :------------------------------------------------------: |
@@ -197,6 +205,7 @@ The Gitea MCP Server supports the following tools:
| edit_issue_comment | Issue | Edit a comment on an issue | | edit_issue_comment | Issue | Edit a comment on an issue |
| get_issue_comments_by_index | Issue | Get comments of an issue by its index | | get_issue_comments_by_index | Issue | Get comments of an issue by its index |
| get_pull_request_by_index | Pull Request | Get a pull request by its index | | get_pull_request_by_index | Pull Request | Get a pull request by its index |
| get_pull_request_diff | Pull Request | Get a pull request diff |
| list_repo_pull_requests | Pull Request | List all pull requests in a repository | | list_repo_pull_requests | Pull Request | List all pull requests in a repository |
| create_pull_request | Pull Request | Create a new pull request | | create_pull_request | Pull Request | Create a new pull request |
| create_pull_request_reviewer | Pull Request | Add reviewers to a pull request | | create_pull_request_reviewer | Pull Request | Add reviewers to a pull request |
@@ -208,6 +217,7 @@ The Gitea MCP Server supports the following tools:
| submit_pull_request_review | Pull Request | Submit a pending review | | submit_pull_request_review | Pull Request | Submit a pending review |
| delete_pull_request_review | Pull Request | Delete a review | | delete_pull_request_review | Pull Request | Delete a review |
| dismiss_pull_request_review | Pull Request | Dismiss a review with optional message | | dismiss_pull_request_review | Pull Request | Dismiss a review with optional message |
| merge_pull_request | Pull Request | Merge a pull request |
| search_users | User | Search for users | | search_users | User | Search for users |
| search_org_teams | Organization | Search for teams in an organization | | search_org_teams | Organization | Search for teams in an organization |
| list_org_labels | Organization | List labels defined at organization level | | list_org_labels | Organization | List labels defined at organization level |
@@ -255,14 +265,14 @@ The Gitea MCP Server supports the following tools:
To enable debug mode, add the `-d` flag when running the Gitea MCP Server with http mode: To enable debug mode, add the `-d` flag when running the Gitea MCP Server with http mode:
```sh ```sh
./gitea-mcp -t http [--port 8080] --token <your personal access token> -d ./gitea-mcp-extended -t http [--port 8080] --token <your personal access token> -d
``` ```
## 🛠 Troubleshooting ## 🛠 Troubleshooting
If you encounter any issues, here are some common troubleshooting steps: If you encounter any issues, here are some common troubleshooting steps:
1. **Check your PATH**: Ensure that the `gitea-mcp` binary is in a directory included in your system's PATH. 1. **Check your PATH**: Ensure that the `gitea-mcp-extended` binary is in a directory included in your system's PATH.
2. **Verify dependencies**: Make sure you have all the required dependencies installed, such as `make` and `Golang`. 2. **Verify dependencies**: Make sure you have all the required dependencies installed, such as `make` and `Golang`.
3. **Review configuration**: Double-check your MCP configuration file for any errors or missing information. 3. **Review configuration**: Double-check your MCP configuration file for any errors or missing information.
4. **Consult logs**: Check the logs for any error messages or warnings that can provide more information about the issue. 4. **Consult logs**: Check the logs for any error messages or warnings that can provide more information about the issue.

View File

@@ -13,6 +13,7 @@
- [什么是 Gitea](#什么是-gitea) - [什么是 Gitea](#什么是-gitea)
- [什么是 MCP](#什么是-mcp) - [什么是 MCP](#什么是-mcp)
- [🚧 安装](#-安装) - [🚧 安装](#-安装)
- [在 Claude Code 中使用](#在-claude-code-中使用)
- [在 VS Code 中使用](#在-vs-code-中使用) - [在 VS Code 中使用](#在-vs-code-中使用)
- [📥 下载官方二进制版本](#-下载官方二进制版本) - [📥 下载官方二进制版本](#-下载官方二进制版本)
- [🔧 从源码构建](#-从源码构建) - [🔧 从源码构建](#-从源码构建)
@@ -32,6 +33,17 @@ Model Context Protocol (MCP) 是一种协议,允许通过聊天界面整合各
## 🚧 安装 ## 🚧 安装
### 在 Claude Code 中使用
此方式使用 `go run`,需要安装 [Go](https://go.dev)。
```bash
claude mcp add --transport stdio --scope user gitea \
--env GITEA_ACCESS_TOKEN=token \
--env GITEA_HOST=https://gitea.com \
-- go run git.lethalbits.com/lethalbits/gitea-mcp@latest -t stdio
```
### 在 VS Code 中使用 ### 在 VS Code 中使用
要快速安装,请使用本 README 顶部的安装按钮。 要快速安装,请使用本 README 顶部的安装按钮。
@@ -75,14 +87,14 @@ Model Context Protocol (MCP) 是一种协议,允许通过聊天界面整合各
### 📥 下载官方二进制版本 ### 📥 下载官方二进制版本
可在 [官方 Gitea MCP 二进制版本](https://gitea.com/gitea/gitea-mcp/releases) 下载。 可在 [官方 Gitea MCP 二进制版本](https://git.lethalbits.com/lethalbits/gitea-mcp/releases) 下载。
### 🔧 从源码构建 ### 🔧 从源码构建
可用 Git 下载源码: 可用 Git 下载源码:
```bash ```bash
git clone https://gitea.com/gitea/gitea-mcp.git git clone https://git.lethalbits.com/lethalbits/gitea-mcp.git
``` ```
构建前请先安装: 构建前请先安装:
@@ -208,6 +220,7 @@ Gitea MCP 服务器支持以下工具:
| submit_pull_request_review | 拉取请求 | 提交待处理的审查 | | submit_pull_request_review | 拉取请求 | 提交待处理的审查 |
| delete_pull_request_review | 拉取请求 | 删除审查 | | delete_pull_request_review | 拉取请求 | 删除审查 |
| dismiss_pull_request_review | 拉取请求 | 驳回审查(可附消息) | | dismiss_pull_request_review | 拉取请求 | 驳回审查(可附消息) |
| merge_pull_request | 拉取请求 | 合并拉取请求 |
| search_users | 用户 | 搜索用户 | | search_users | 用户 | 搜索用户 |
| search_org_teams | 组织 | 搜索组织团队 | | search_org_teams | 组织 | 搜索组织团队 |
| list_org_labels | 组织 | 列出组织标签 | | list_org_labels | 组织 | 列出组织标签 |

View File

@@ -13,6 +13,7 @@
- [什麼是 Gitea](#什麼是-gitea) - [什麼是 Gitea](#什麼是-gitea)
- [什麼是 MCP](#什麼是-mcp) - [什麼是 MCP](#什麼是-mcp)
- [🚧 安裝](#-安裝) - [🚧 安裝](#-安裝)
- [在 Claude Code 中使用](#在-claude-code-中使用)
- [在 VS Code 中使用](#在-vs-code-中使用) - [在 VS Code 中使用](#在-vs-code-中使用)
- [📥 下載官方二進位版本](#-下載官方二進位版本) - [📥 下載官方二進位版本](#-下載官方二進位版本)
- [🔧 從原始碼建置](#-從原始碼建置) - [🔧 從原始碼建置](#-從原始碼建置)
@@ -32,6 +33,17 @@ Model Context Protocol (MCP) 是一種協議,允許透過聊天介面整合各
## 🚧 安裝 ## 🚧 安裝
### 在 Claude Code 中使用
此方式使用 `go run`,需要安裝 [Go](https://go.dev)。
```bash
claude mcp add --transport stdio --scope user gitea \
--env GITEA_ACCESS_TOKEN=token \
--env GITEA_HOST=https://gitea.com \
-- go run git.lethalbits.com/lethalbits/gitea-mcp@latest -t stdio
```
### 在 VS Code 中使用 ### 在 VS Code 中使用
欲快速安裝,請使用本 README 頂部的安裝按鈕。 欲快速安裝,請使用本 README 頂部的安裝按鈕。
@@ -75,14 +87,14 @@ Model Context Protocol (MCP) 是一種協議,允許透過聊天介面整合各
### 📥 下載官方二進位版本 ### 📥 下載官方二進位版本
可至 [官方 Gitea MCP 二進位版本](https://gitea.com/gitea/gitea-mcp/releases) 下載。 可至 [官方 Gitea MCP 二進位版本](https://git.lethalbits.com/lethalbits/gitea-mcp/releases) 下載。
### 🔧 從原始碼建置 ### 🔧 從原始碼建置
可用 Git 下載原始碼: 可用 Git 下載原始碼:
```bash ```bash
git clone https://gitea.com/gitea/gitea-mcp.git git clone https://git.lethalbits.com/lethalbits/gitea-mcp.git
``` ```
建置前請先安裝: 建置前請先安裝:
@@ -208,6 +220,7 @@ Gitea MCP 伺服器支援以下工具:
| submit_pull_request_review | 拉取請求 | 提交待處理的審查 | | submit_pull_request_review | 拉取請求 | 提交待處理的審查 |
| delete_pull_request_review | 拉取請求 | 刪除審查 | | delete_pull_request_review | 拉取請求 | 刪除審查 |
| dismiss_pull_request_review | 拉取請求 | 駁回審查(可附訊息) | | dismiss_pull_request_review | 拉取請求 | 駁回審查(可附訊息) |
| merge_pull_request | 拉取請求 | 合併拉取請求 |
| search_users | 用戶 | 搜尋用戶 | | search_users | 用戶 | 搜尋用戶 |
| search_org_teams | 組織 | 搜尋組織團隊 | | search_org_teams | 組織 | 搜尋組織團隊 |
| list_org_labels | 組織 | 列出組織標籤 | | list_org_labels | 組織 | 列出組織標籤 |

View File

@@ -3,68 +3,63 @@ package cmd
import ( import (
"context" "context"
"flag" "flag"
"fmt"
"os" "os"
"text/tabwriter"
"gitea.com/gitea/gitea-mcp/operation" "git.lethalbits.com/lethalbits/gitea-mcp-extended/operation"
flagPkg "gitea.com/gitea/gitea-mcp/pkg/flag" flagPkg "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/flag"
"gitea.com/gitea/gitea-mcp/pkg/log" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/log"
) )
var ( var (
host string host string
port int port int
token string token string
version bool
) )
func init() { func init() {
flag.StringVar( flag.StringVar(&flagPkg.Mode, "t", "stdio", "")
&flagPkg.Mode, flag.StringVar(&flagPkg.Mode, "transport", "stdio", "")
"t", flag.StringVar(&host, "H", os.Getenv("GITEA_HOST"), "")
"stdio", flag.StringVar(&host, "host", os.Getenv("GITEA_HOST"), "")
"Transport type (stdio or http)", flag.IntVar(&port, "p", 8080, "")
) flag.IntVar(&port, "port", 8080, "")
flag.StringVar( flag.StringVar(&token, "T", "", "")
&flagPkg.Mode, flag.StringVar(&token, "token", "", "")
"transport", flag.BoolVar(&flagPkg.ReadOnly, "r", false, "")
"stdio", flag.BoolVar(&flagPkg.ReadOnly, "read-only", false, "")
"Transport type (stdio or http)", flag.BoolVar(&flagPkg.Debug, "d", false, "")
) flag.BoolVar(&flagPkg.Debug, "debug", false, "")
flag.StringVar( flag.BoolVar(&flagPkg.Insecure, "k", false, "")
&host, flag.BoolVar(&flagPkg.Insecure, "insecure", false, "")
"host", flag.BoolVar(&version, "v", false, "")
os.Getenv("GITEA_HOST"), flag.BoolVar(&version, "version", false, "")
"Gitea host",
) flag.Usage = func() {
flag.IntVar( w := tabwriter.NewWriter(os.Stderr, 0, 0, 3, ' ', 0)
&port, fmt.Fprintln(os.Stderr, "Usage: gitea-mcp-extended [options]")
"port", fmt.Fprintln(os.Stderr)
8080, fmt.Fprintln(os.Stderr, "Options:")
"http port", fmt.Fprintf(w, " -t, -transport <type>\tTransport type: stdio or http (default: stdio)\n")
) fmt.Fprintf(w, " -H, -host <url>\tGitea host URL (default: https://gitea.com)\n")
flag.StringVar( fmt.Fprintf(w, " -p, -port <number>\tHTTP server port (default: 8080)\n")
&token, fmt.Fprintf(w, " -T, -token <token>\tPersonal access token\n")
"token", fmt.Fprintf(w, " -r, -read-only\tExpose only read-only tools\n")
"", fmt.Fprintf(w, " -d, -debug\tEnable debug mode\n")
"Your personal access token", fmt.Fprintf(w, " -k, -insecure\tIgnore TLS certificate errors\n")
) fmt.Fprintf(w, " -v, -version\tPrint version and exit\n")
flag.BoolVar( fmt.Fprintln(w)
&flagPkg.ReadOnly, fmt.Fprintln(w, "Environment variables:")
"read-only", fmt.Fprintf(w, " GITEA_ACCESS_TOKEN\tProvide access token\n")
false, fmt.Fprintf(w, " GITEA_DEBUG\tSet to 'true' for debug mode\n")
"Read-only mode", fmt.Fprintf(w, " GITEA_HOST\tOverride Gitea host URL\n")
) fmt.Fprintf(w, " GITEA_INSECURE\tSet to 'true' to ignore TLS errors\n")
flag.BoolVar( fmt.Fprintf(w, " GITEA_READONLY\tSet to 'true' for read-only mode\n")
&flagPkg.Debug, fmt.Fprintf(w, " MCP_MODE\tOverride transport mode\n")
"d", w.Flush()
false, }
"debug mode (If -d flag is provided, debug mode will be enabled by default)",
)
flag.BoolVar(
&flagPkg.Insecure,
"insecure",
false,
"ignore TLS certificate errors",
)
flag.Parse() flag.Parse()
@@ -99,12 +94,16 @@ func init() {
} }
func Execute() { func Execute() {
defer log.Default().Sync() if version {
fmt.Fprintln(os.Stdout, flagPkg.Version)
return
}
defer log.Default().Sync() //nolint:errcheck // best-effort flush
if err := operation.Run(); err != nil { if err := operation.Run(); err != nil {
if err == context.Canceled { if err == context.Canceled {
log.Info("Server shutdown due to context cancellation") log.Info("Server shutdown due to context cancellation")
return return
} }
log.Fatalf("Run Gitea MCP Server Error: %v", err) log.Fatalf("Run Gitea MCP Server Error: %v", err) //nolint:gocritic // intentional exit after defer
} }
} }

16
go.mod
View File

@@ -1,11 +1,11 @@
module gitea.com/gitea/gitea-mcp module git.lethalbits.com/lethalbits/gitea-mcp-extended
go 1.24.0 go 1.26.0
require ( require (
code.gitea.io/sdk/gitea v0.22.1 code.gitea.io/sdk/gitea v0.23.2
github.com/mark3labs/mcp-go v0.42.0 github.com/mark3labs/mcp-go v0.44.0
go.uber.org/zap v1.27.0 go.uber.org/zap v1.27.1
gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/natefinch/lumberjack.v2 v2.2.1
) )
@@ -16,14 +16,14 @@ require (
github.com/davidmz/go-pageant v1.0.2 // indirect github.com/davidmz/go-pageant v1.0.2 // indirect
github.com/go-fed/httpsig v1.1.0 // indirect github.com/go-fed/httpsig v1.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/go-version v1.7.0 // indirect github.com/hashicorp/go-version v1.8.0 // indirect
github.com/invopop/jsonschema v0.13.0 // indirect github.com/invopop/jsonschema v0.13.0 // indirect
github.com/mailru/easyjson v0.9.1 // indirect github.com/mailru/easyjson v0.9.1 // indirect
github.com/spf13/cast v1.10.0 // indirect github.com/spf13/cast v1.10.0 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.43.0 // indirect golang.org/x/crypto v0.48.0 // indirect
golang.org/x/sys v0.37.0 // indirect golang.org/x/sys v0.41.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

28
go.sum
View File

@@ -1,5 +1,5 @@
code.gitea.io/sdk/gitea v0.22.1 h1:7K05KjRORyTcTYULQ/AwvlVS6pawLcWyXZcTr7gHFyA= code.gitea.io/sdk/gitea v0.23.2 h1:iJB1FDmLegwfwjX8gotBDHdPSbk/ZR8V9VmEJaVsJYg=
code.gitea.io/sdk/gitea v0.22.1/go.mod h1:yyF5+GhljqvA30sRDreoyHILruNiy4ASufugzYg0VHM= code.gitea.io/sdk/gitea v0.23.2/go.mod h1:yyF5+GhljqvA30sRDreoyHILruNiy4ASufugzYg0VHM=
github.com/42wim/httpsig v1.2.3 h1:xb0YyWhkYj57SPtfSttIobJUPJZB9as1nsfo7KWVcEs= github.com/42wim/httpsig v1.2.3 h1:xb0YyWhkYj57SPtfSttIobJUPJZB9as1nsfo7KWVcEs=
github.com/42wim/httpsig v1.2.3/go.mod h1:nZq9OlYKDrUBhptd77IHx4/sZZD+IxTBADvAPI9G/EM= github.com/42wim/httpsig v1.2.3/go.mod h1:nZq9OlYKDrUBhptd77IHx4/sZZD+IxTBADvAPI9G/EM=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
@@ -18,8 +18,8 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4=
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -28,8 +28,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8=
github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mark3labs/mcp-go v0.42.0 h1:gk/8nYJh8t3yroCAOBhNbYsM9TCKvkM13I5t5Hfu6Ls= github.com/mark3labs/mcp-go v0.44.0 h1:OlYfcVviAnwNN40QZUrrzU0QZjq3En7rCU5X09a/B7I=
github.com/mark3labs/mcp-go v0.42.0/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw= github.com/mark3labs/mcp-go v0.44.0/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
@@ -46,23 +46,23 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

View File

@@ -1,13 +1,11 @@
package main package main
import ( import (
"gitea.com/gitea/gitea-mcp/cmd" "git.lethalbits.com/lethalbits/gitea-mcp-extended/cmd"
"gitea.com/gitea/gitea-mcp/pkg/flag" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/flag"
) )
var ( var Version = "dev"
Version = "dev"
)
func init() { func init() {
flag.Version = Version flag.Version = Version

View File

@@ -1,10 +1,8 @@
package actions package actions
import ( import (
"gitea.com/gitea/gitea-mcp/pkg/tool" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/tool"
) )
// Tool is the registry for all Actions-related MCP tools. // Tool is the registry for all Actions-related MCP tools.
var Tool = tool.New() var Tool = tool.New()

View File

@@ -4,13 +4,15 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"net/http"
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
"gitea.com/gitea/gitea-mcp/pkg/gitea" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/gitea"
"gitea.com/gitea/gitea-mcp/pkg/log" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/log"
"gitea.com/gitea/gitea-mcp/pkg/to" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/params"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/to"
"github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server" "github.com/mark3labs/mcp-go/server"
@@ -67,7 +69,7 @@ func fetchJobLogBytes(ctx context.Context, owner, repo string, jobID int64) ([]b
} }
lastErr = err lastErr = err
var httpErr *gitea.HTTPError var httpErr *gitea.HTTPError
if errors.As(err, &httpErr) && (httpErr.StatusCode == 404 || httpErr.StatusCode == 405) { if errors.As(err, &httpErr) && (httpErr.StatusCode == http.StatusNotFound || httpErr.StatusCode == http.StatusMethodNotAllowed) {
continue continue
} }
return nil, p, err return nil, p, err
@@ -109,28 +111,18 @@ func GetRepoActionJobLogPreviewFn(ctx context.Context, req mcp.CallToolRequest)
log.Debugf("Called GetRepoActionJobLogPreviewFn") log.Debugf("Called GetRepoActionJobLogPreviewFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok || owner == "" { if !ok || owner == "" {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok || repo == "" { if !ok || repo == "" {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
jobIDFloat, ok := req.GetArguments()["job_id"].(float64) jobID, err := params.GetIndex(req.GetArguments(), "job_id")
if !ok || jobIDFloat <= 0 { if err != nil || jobID <= 0 {
return to.ErrorResult(fmt.Errorf("job_id is required")) return to.ErrorResult(errors.New("job_id is required"))
} }
tailLinesFloat, _ := req.GetArguments()["tail_lines"].(float64) tailLines := int(params.GetOptionalInt(req.GetArguments(), "tail_lines", 200))
maxBytesFloat, _ := req.GetArguments()["max_bytes"].(float64) maxBytes := int(params.GetOptionalInt(req.GetArguments(), "max_bytes", 65536))
tailLines := int(tailLinesFloat)
if tailLines <= 0 {
tailLines = 200
}
maxBytes := int(maxBytesFloat)
if maxBytes <= 0 {
maxBytes = 65536
}
jobID := int64(jobIDFloat)
raw, usedPath, err := fetchJobLogBytes(ctx, owner, repo, jobID) raw, usedPath, err := fetchJobLogBytes(ctx, owner, repo, jobID)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("get job log err: %v", err)) return to.ErrorResult(fmt.Errorf("get job log err: %v", err))
@@ -140,13 +132,13 @@ func GetRepoActionJobLogPreviewFn(ctx context.Context, req mcp.CallToolRequest)
limited, truncated := limitBytes(tailed, maxBytes) limited, truncated := limitBytes(tailed, maxBytes)
return to.TextResult(map[string]any{ return to.TextResult(map[string]any{
"endpoint": usedPath, "endpoint": usedPath,
"job_id": jobID, "job_id": jobID,
"bytes": len(raw), "bytes": len(raw),
"tail_lines": tailLines, "tail_lines": tailLines,
"max_bytes": maxBytes, "max_bytes": maxBytes,
"truncated": truncated, "truncated": truncated,
"log": string(limited), "log": string(limited),
}) })
} }
@@ -154,18 +146,17 @@ func DownloadRepoActionJobLogFn(ctx context.Context, req mcp.CallToolRequest) (*
log.Debugf("Called DownloadRepoActionJobLogFn") log.Debugf("Called DownloadRepoActionJobLogFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok || owner == "" { if !ok || owner == "" {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok || repo == "" { if !ok || repo == "" {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
jobIDFloat, ok := req.GetArguments()["job_id"].(float64) jobID, err := params.GetIndex(req.GetArguments(), "job_id")
if !ok || jobIDFloat <= 0 { if err != nil || jobID <= 0 {
return to.ErrorResult(fmt.Errorf("job_id is required")) return to.ErrorResult(errors.New("job_id is required"))
} }
outputPath, _ := req.GetArguments()["output_path"].(string) outputPath, _ := req.GetArguments()["output_path"].(string)
jobID := int64(jobIDFloat)
raw, usedPath, err := fetchJobLogBytes(ctx, owner, repo, jobID) raw, usedPath, err := fetchJobLogBytes(ctx, owner, repo, jobID)
if err != nil { if err != nil {
@@ -194,5 +185,3 @@ func DownloadRepoActionJobLogFn(ctx context.Context, req mcp.CallToolRequest) (*
"bytes": len(raw), "bytes": len(raw),
}) })
} }

View File

@@ -20,5 +20,3 @@ func TestLimitBytesKeepsTail(t *testing.T) {
t.Fatalf("limitBytes tail = %q, want %q", string(out), "6789") t.Fatalf("limitBytes tail = %q, want %q", string(out), "6789")
} }
} }

View File

@@ -4,11 +4,15 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"maps"
"net/http"
"net/url" "net/url"
"strconv"
"gitea.com/gitea/gitea-mcp/pkg/gitea" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/gitea"
"gitea.com/gitea/gitea-mcp/pkg/log" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/log"
"gitea.com/gitea/gitea-mcp/pkg/to" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/params"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/to"
"github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server" "github.com/mark3labs/mcp-go/server"
@@ -125,47 +129,41 @@ func init() {
Tool.RegisterRead(server.ServerTool{Tool: ListRepoActionRunJobsTool, Handler: ListRepoActionRunJobsFn}) Tool.RegisterRead(server.ServerTool{Tool: ListRepoActionRunJobsTool, Handler: ListRepoActionRunJobsFn})
} }
func doJSONWithFallback(ctx context.Context, method string, paths []string, query url.Values, body any, respOut any) (string, int, error) { func doJSONWithFallback(ctx context.Context, method string, paths []string, query url.Values, body, respOut any) error {
var lastErr error var lastErr error
for _, p := range paths { for _, p := range paths {
status, err := gitea.DoJSON(ctx, method, p, query, body, respOut) _, err := gitea.DoJSON(ctx, method, p, query, body, respOut)
if err == nil { if err == nil {
return p, status, nil return nil
} }
lastErr = err lastErr = err
var httpErr *gitea.HTTPError var httpErr *gitea.HTTPError
if errors.As(err, &httpErr) && (httpErr.StatusCode == 404 || httpErr.StatusCode == 405) { if errors.As(err, &httpErr) && (httpErr.StatusCode == http.StatusNotFound || httpErr.StatusCode == http.StatusMethodNotAllowed) {
continue continue
} }
return p, status, err return err
} }
return "", 0, lastErr return lastErr
} }
func ListRepoActionWorkflowsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func ListRepoActionWorkflowsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListRepoActionWorkflowsFn") log.Debugf("Called ListRepoActionWorkflowsFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok || owner == "" { if !ok || owner == "" {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok || repo == "" { if !ok || repo == "" {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
}
page, _ := req.GetArguments()["page"].(float64)
if page <= 0 {
page = 1
}
pageSize, _ := req.GetArguments()["pageSize"].(float64)
if pageSize <= 0 {
pageSize = 50
} }
page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 50)
query := url.Values{} query := url.Values{}
query.Set("page", fmt.Sprintf("%d", int(page))) query.Set("page", strconv.Itoa(int(page)))
query.Set("limit", fmt.Sprintf("%d", int(pageSize))) query.Set("limit", strconv.Itoa(int(pageSize)))
var result any var result any
_, _, err := doJSONWithFallback(ctx, "GET", err := doJSONWithFallback(ctx, "GET",
[]string{ []string{
fmt.Sprintf("repos/%s/%s/actions/workflows", url.PathEscape(owner), url.PathEscape(repo)), fmt.Sprintf("repos/%s/%s/actions/workflows", url.PathEscape(owner), url.PathEscape(repo)),
}, },
@@ -181,19 +179,19 @@ func GetRepoActionWorkflowFn(ctx context.Context, req mcp.CallToolRequest) (*mcp
log.Debugf("Called GetRepoActionWorkflowFn") log.Debugf("Called GetRepoActionWorkflowFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok || owner == "" { if !ok || owner == "" {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok || repo == "" { if !ok || repo == "" {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
workflowID, ok := req.GetArguments()["workflow_id"].(string) workflowID, ok := req.GetArguments()["workflow_id"].(string)
if !ok || workflowID == "" { if !ok || workflowID == "" {
return to.ErrorResult(fmt.Errorf("workflow_id is required")) return to.ErrorResult(errors.New("workflow_id is required"))
} }
var result any var result any
_, _, err := doJSONWithFallback(ctx, "GET", err := doJSONWithFallback(ctx, "GET",
[]string{ []string{
fmt.Sprintf("repos/%s/%s/actions/workflows/%s", url.PathEscape(owner), url.PathEscape(repo), url.PathEscape(workflowID)), fmt.Sprintf("repos/%s/%s/actions/workflows/%s", url.PathEscape(owner), url.PathEscape(repo), url.PathEscape(workflowID)),
}, },
@@ -209,30 +207,28 @@ func DispatchRepoActionWorkflowFn(ctx context.Context, req mcp.CallToolRequest)
log.Debugf("Called DispatchRepoActionWorkflowFn") log.Debugf("Called DispatchRepoActionWorkflowFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok || owner == "" { if !ok || owner == "" {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok || repo == "" { if !ok || repo == "" {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
workflowID, ok := req.GetArguments()["workflow_id"].(string) workflowID, ok := req.GetArguments()["workflow_id"].(string)
if !ok || workflowID == "" { if !ok || workflowID == "" {
return to.ErrorResult(fmt.Errorf("workflow_id is required")) return to.ErrorResult(errors.New("workflow_id is required"))
} }
ref, ok := req.GetArguments()["ref"].(string) ref, ok := req.GetArguments()["ref"].(string)
if !ok || ref == "" { if !ok || ref == "" {
return to.ErrorResult(fmt.Errorf("ref is required")) return to.ErrorResult(errors.New("ref is required"))
} }
var inputs map[string]any var inputs map[string]any
if raw, exists := req.GetArguments()["inputs"]; exists { if raw, exists := req.GetArguments()["inputs"]; exists {
if m, ok := raw.(map[string]any); ok { if m, ok := raw.(map[string]any); ok {
inputs = m inputs = m
} else if m, ok := raw.(map[string]interface{}); ok { } else if m, ok := raw.(map[string]any); ok {
inputs = make(map[string]any, len(m)) inputs = make(map[string]any, len(m))
for k, v := range m { maps.Copy(inputs, m)
inputs[k] = v
}
} }
} }
@@ -243,7 +239,7 @@ func DispatchRepoActionWorkflowFn(ctx context.Context, req mcp.CallToolRequest)
body["inputs"] = inputs body["inputs"] = inputs
} }
_, _, err := doJSONWithFallback(ctx, "POST", err := doJSONWithFallback(ctx, "POST",
[]string{ []string{
fmt.Sprintf("repos/%s/%s/actions/workflows/%s/dispatches", url.PathEscape(owner), url.PathEscape(repo), url.PathEscape(workflowID)), fmt.Sprintf("repos/%s/%s/actions/workflows/%s/dispatches", url.PathEscape(owner), url.PathEscape(repo), url.PathEscape(workflowID)),
fmt.Sprintf("repos/%s/%s/actions/workflows/%s/dispatch", url.PathEscape(owner), url.PathEscape(repo), url.PathEscape(workflowID)), fmt.Sprintf("repos/%s/%s/actions/workflows/%s/dispatch", url.PathEscape(owner), url.PathEscape(repo), url.PathEscape(workflowID)),
@@ -252,7 +248,7 @@ func DispatchRepoActionWorkflowFn(ctx context.Context, req mcp.CallToolRequest)
) )
if err != nil { if err != nil {
var httpErr *gitea.HTTPError var httpErr *gitea.HTTPError
if errors.As(err, &httpErr) && (httpErr.StatusCode == 404 || httpErr.StatusCode == 405) { if errors.As(err, &httpErr) && (httpErr.StatusCode == http.StatusNotFound || httpErr.StatusCode == http.StatusMethodNotAllowed) {
return to.ErrorResult(fmt.Errorf("workflow dispatch not supported on this Gitea version (endpoint returned %d). Check https://docs.gitea.com/api/1.24/ for available Actions endpoints", httpErr.StatusCode)) return to.ErrorResult(fmt.Errorf("workflow dispatch not supported on this Gitea version (endpoint returned %d). Check https://docs.gitea.com/api/1.24/ for available Actions endpoints", httpErr.StatusCode))
} }
return to.ErrorResult(fmt.Errorf("dispatch action workflow err: %v", err)) return to.ErrorResult(fmt.Errorf("dispatch action workflow err: %v", err))
@@ -264,31 +260,25 @@ func ListRepoActionRunsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Ca
log.Debugf("Called ListRepoActionRunsFn") log.Debugf("Called ListRepoActionRunsFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok || owner == "" { if !ok || owner == "" {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok || repo == "" { if !ok || repo == "" {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
}
page, _ := req.GetArguments()["page"].(float64)
if page <= 0 {
page = 1
}
pageSize, _ := req.GetArguments()["pageSize"].(float64)
if pageSize <= 0 {
pageSize = 50
} }
page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 50)
statusFilter, _ := req.GetArguments()["status"].(string) statusFilter, _ := req.GetArguments()["status"].(string)
query := url.Values{} query := url.Values{}
query.Set("page", fmt.Sprintf("%d", int(page))) query.Set("page", strconv.Itoa(int(page)))
query.Set("limit", fmt.Sprintf("%d", int(pageSize))) query.Set("limit", strconv.Itoa(int(pageSize)))
if statusFilter != "" { if statusFilter != "" {
query.Set("status", statusFilter) query.Set("status", statusFilter)
} }
var result any var result any
_, _, err := doJSONWithFallback(ctx, "GET", err := doJSONWithFallback(ctx, "GET",
[]string{ []string{
fmt.Sprintf("repos/%s/%s/actions/runs", url.PathEscape(owner), url.PathEscape(repo)), fmt.Sprintf("repos/%s/%s/actions/runs", url.PathEscape(owner), url.PathEscape(repo)),
}, },
@@ -304,21 +294,21 @@ func GetRepoActionRunFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call
log.Debugf("Called GetRepoActionRunFn") log.Debugf("Called GetRepoActionRunFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok || owner == "" { if !ok || owner == "" {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok || repo == "" { if !ok || repo == "" {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
runID, ok := req.GetArguments()["run_id"].(float64) runID, err := params.GetIndex(req.GetArguments(), "run_id")
if !ok || runID <= 0 { if err != nil || runID <= 0 {
return to.ErrorResult(fmt.Errorf("run_id is required")) return to.ErrorResult(errors.New("run_id is required"))
} }
var result any var result any
_, _, err := doJSONWithFallback(ctx, "GET", err = doJSONWithFallback(ctx, "GET",
[]string{ []string{
fmt.Sprintf("repos/%s/%s/actions/runs/%d", url.PathEscape(owner), url.PathEscape(repo), int64(runID)), fmt.Sprintf("repos/%s/%s/actions/runs/%d", url.PathEscape(owner), url.PathEscape(repo), runID),
}, },
nil, nil, &result, nil, nil, &result,
) )
@@ -332,20 +322,20 @@ func CancelRepoActionRunFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.C
log.Debugf("Called CancelRepoActionRunFn") log.Debugf("Called CancelRepoActionRunFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok || owner == "" { if !ok || owner == "" {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok || repo == "" { if !ok || repo == "" {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
runID, ok := req.GetArguments()["run_id"].(float64) runID, err := params.GetIndex(req.GetArguments(), "run_id")
if !ok || runID <= 0 { if err != nil || runID <= 0 {
return to.ErrorResult(fmt.Errorf("run_id is required")) return to.ErrorResult(errors.New("run_id is required"))
} }
_, _, err := doJSONWithFallback(ctx, "POST", err = doJSONWithFallback(ctx, "POST",
[]string{ []string{
fmt.Sprintf("repos/%s/%s/actions/runs/%d/cancel", url.PathEscape(owner), url.PathEscape(repo), int64(runID)), fmt.Sprintf("repos/%s/%s/actions/runs/%d/cancel", url.PathEscape(owner), url.PathEscape(repo), runID),
}, },
nil, nil, nil, nil, nil, nil,
) )
@@ -359,27 +349,27 @@ func RerunRepoActionRunFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Ca
log.Debugf("Called RerunRepoActionRunFn") log.Debugf("Called RerunRepoActionRunFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok || owner == "" { if !ok || owner == "" {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok || repo == "" { if !ok || repo == "" {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
runID, ok := req.GetArguments()["run_id"].(float64) runID, err := params.GetIndex(req.GetArguments(), "run_id")
if !ok || runID <= 0 { if err != nil || runID <= 0 {
return to.ErrorResult(fmt.Errorf("run_id is required")) return to.ErrorResult(errors.New("run_id is required"))
} }
_, _, err := doJSONWithFallback(ctx, "POST", err = doJSONWithFallback(ctx, "POST",
[]string{ []string{
fmt.Sprintf("repos/%s/%s/actions/runs/%d/rerun", url.PathEscape(owner), url.PathEscape(repo), int64(runID)), fmt.Sprintf("repos/%s/%s/actions/runs/%d/rerun", url.PathEscape(owner), url.PathEscape(repo), runID),
fmt.Sprintf("repos/%s/%s/actions/runs/%d/rerun-failed-jobs", url.PathEscape(owner), url.PathEscape(repo), int64(runID)), fmt.Sprintf("repos/%s/%s/actions/runs/%d/rerun-failed-jobs", url.PathEscape(owner), url.PathEscape(repo), runID),
}, },
nil, nil, nil, nil, nil, nil,
) )
if err != nil { if err != nil {
var httpErr *gitea.HTTPError var httpErr *gitea.HTTPError
if errors.As(err, &httpErr) && (httpErr.StatusCode == 404 || httpErr.StatusCode == 405) { if errors.As(err, &httpErr) && (httpErr.StatusCode == http.StatusNotFound || httpErr.StatusCode == http.StatusMethodNotAllowed) {
return to.ErrorResult(fmt.Errorf("workflow rerun not supported on this Gitea version (endpoint returned %d). Check https://docs.gitea.com/api/1.24/ for available Actions endpoints", httpErr.StatusCode)) return to.ErrorResult(fmt.Errorf("workflow rerun not supported on this Gitea version (endpoint returned %d). Check https://docs.gitea.com/api/1.24/ for available Actions endpoints", httpErr.StatusCode))
} }
return to.ErrorResult(fmt.Errorf("rerun action run err: %v", err)) return to.ErrorResult(fmt.Errorf("rerun action run err: %v", err))
@@ -391,31 +381,25 @@ func ListRepoActionJobsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Ca
log.Debugf("Called ListRepoActionJobsFn") log.Debugf("Called ListRepoActionJobsFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok || owner == "" { if !ok || owner == "" {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok || repo == "" { if !ok || repo == "" {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
}
page, _ := req.GetArguments()["page"].(float64)
if page <= 0 {
page = 1
}
pageSize, _ := req.GetArguments()["pageSize"].(float64)
if pageSize <= 0 {
pageSize = 50
} }
page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 50)
statusFilter, _ := req.GetArguments()["status"].(string) statusFilter, _ := req.GetArguments()["status"].(string)
query := url.Values{} query := url.Values{}
query.Set("page", fmt.Sprintf("%d", int(page))) query.Set("page", strconv.Itoa(int(page)))
query.Set("limit", fmt.Sprintf("%d", int(pageSize))) query.Set("limit", strconv.Itoa(int(pageSize)))
if statusFilter != "" { if statusFilter != "" {
query.Set("status", statusFilter) query.Set("status", statusFilter)
} }
var result any var result any
_, _, err := doJSONWithFallback(ctx, "GET", err := doJSONWithFallback(ctx, "GET",
[]string{ []string{
fmt.Sprintf("repos/%s/%s/actions/jobs", url.PathEscape(owner), url.PathEscape(repo)), fmt.Sprintf("repos/%s/%s/actions/jobs", url.PathEscape(owner), url.PathEscape(repo)),
}, },
@@ -431,33 +415,27 @@ func ListRepoActionRunJobsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp
log.Debugf("Called ListRepoActionRunJobsFn") log.Debugf("Called ListRepoActionRunJobsFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok || owner == "" { if !ok || owner == "" {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok || repo == "" { if !ok || repo == "" {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
runID, ok := req.GetArguments()["run_id"].(float64) runID, err := params.GetIndex(req.GetArguments(), "run_id")
if !ok || runID <= 0 { if err != nil || runID <= 0 {
return to.ErrorResult(fmt.Errorf("run_id is required")) return to.ErrorResult(errors.New("run_id is required"))
}
page, _ := req.GetArguments()["page"].(float64)
if page <= 0 {
page = 1
}
pageSize, _ := req.GetArguments()["pageSize"].(float64)
if pageSize <= 0 {
pageSize = 50
} }
page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 50)
query := url.Values{} query := url.Values{}
query.Set("page", fmt.Sprintf("%d", int(page))) query.Set("page", strconv.Itoa(int(page)))
query.Set("limit", fmt.Sprintf("%d", int(pageSize))) query.Set("limit", strconv.Itoa(int(pageSize)))
var result any var result any
_, _, err := doJSONWithFallback(ctx, "GET", err = doJSONWithFallback(ctx, "GET",
[]string{ []string{
fmt.Sprintf("repos/%s/%s/actions/runs/%d/jobs", url.PathEscape(owner), url.PathEscape(repo), int64(runID)), fmt.Sprintf("repos/%s/%s/actions/runs/%d/jobs", url.PathEscape(owner), url.PathEscape(repo), runID),
}, },
query, nil, &result, query, nil, &result,
) )

View File

@@ -2,13 +2,15 @@ package actions
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"net/url" "net/url"
"time" "time"
"gitea.com/gitea/gitea-mcp/pkg/gitea" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/gitea"
"gitea.com/gitea/gitea-mcp/pkg/log" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/log"
"gitea.com/gitea/gitea-mcp/pkg/to" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/params"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/to"
gitea_sdk "code.gitea.io/sdk/gitea" gitea_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/mcp"
@@ -27,7 +29,7 @@ const (
type secretMeta struct { type secretMeta struct {
Name string `json:"name"` Name string `json:"name"`
Description string `json:"description,omitempty"` Description string `json:"description,omitempty"`
CreatedAt time.Time `json:"created_at,omitempty"` CreatedAt time.Time `json:"created_at,omitzero"`
} }
var ( var (
@@ -97,20 +99,14 @@ func ListRepoActionSecretsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp
log.Debugf("Called ListRepoActionSecretsFn") log.Debugf("Called ListRepoActionSecretsFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok || owner == "" { if !ok || owner == "" {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok || repo == "" { if !ok || repo == "" {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
}
page, _ := req.GetArguments()["page"].(float64)
if page <= 0 {
page = 1
}
pageSize, _ := req.GetArguments()["pageSize"].(float64)
if pageSize <= 0 {
pageSize = 100
} }
page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
if err != nil { if err != nil {
@@ -142,19 +138,19 @@ func UpsertRepoActionSecretFn(ctx context.Context, req mcp.CallToolRequest) (*mc
log.Debugf("Called UpsertRepoActionSecretFn") log.Debugf("Called UpsertRepoActionSecretFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok || owner == "" { if !ok || owner == "" {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok || repo == "" { if !ok || repo == "" {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
name, ok := req.GetArguments()["name"].(string) name, ok := req.GetArguments()["name"].(string)
if !ok || name == "" { if !ok || name == "" {
return to.ErrorResult(fmt.Errorf("name is required")) return to.ErrorResult(errors.New("name is required"))
} }
data, ok := req.GetArguments()["data"].(string) data, ok := req.GetArguments()["data"].(string)
if !ok || data == "" { if !ok || data == "" {
return to.ErrorResult(fmt.Errorf("data is required")) return to.ErrorResult(errors.New("data is required"))
} }
description, _ := req.GetArguments()["description"].(string) description, _ := req.GetArguments()["description"].(string)
@@ -177,15 +173,15 @@ func DeleteRepoActionSecretFn(ctx context.Context, req mcp.CallToolRequest) (*mc
log.Debugf("Called DeleteRepoActionSecretFn") log.Debugf("Called DeleteRepoActionSecretFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok || owner == "" { if !ok || owner == "" {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok || repo == "" { if !ok || repo == "" {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
secretName, ok := req.GetArguments()["secretName"].(string) secretName, ok := req.GetArguments()["secretName"].(string)
if !ok || secretName == "" { if !ok || secretName == "" {
return to.ErrorResult(fmt.Errorf("secretName is required")) return to.ErrorResult(errors.New("secretName is required"))
} }
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
@@ -203,16 +199,10 @@ func ListOrgActionSecretsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.
log.Debugf("Called ListOrgActionSecretsFn") log.Debugf("Called ListOrgActionSecretsFn")
org, ok := req.GetArguments()["org"].(string) org, ok := req.GetArguments()["org"].(string)
if !ok || org == "" { if !ok || org == "" {
return to.ErrorResult(fmt.Errorf("org is required")) return to.ErrorResult(errors.New("org is required"))
}
page, _ := req.GetArguments()["page"].(float64)
if page <= 0 {
page = 1
}
pageSize, _ := req.GetArguments()["pageSize"].(float64)
if pageSize <= 0 {
pageSize = 100
} }
page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
if err != nil { if err != nil {
@@ -244,15 +234,15 @@ func UpsertOrgActionSecretFn(ctx context.Context, req mcp.CallToolRequest) (*mcp
log.Debugf("Called UpsertOrgActionSecretFn") log.Debugf("Called UpsertOrgActionSecretFn")
org, ok := req.GetArguments()["org"].(string) org, ok := req.GetArguments()["org"].(string)
if !ok || org == "" { if !ok || org == "" {
return to.ErrorResult(fmt.Errorf("org is required")) return to.ErrorResult(errors.New("org is required"))
} }
name, ok := req.GetArguments()["name"].(string) name, ok := req.GetArguments()["name"].(string)
if !ok || name == "" { if !ok || name == "" {
return to.ErrorResult(fmt.Errorf("name is required")) return to.ErrorResult(errors.New("name is required"))
} }
data, ok := req.GetArguments()["data"].(string) data, ok := req.GetArguments()["data"].(string)
if !ok || data == "" { if !ok || data == "" {
return to.ErrorResult(fmt.Errorf("data is required")) return to.ErrorResult(errors.New("data is required"))
} }
description, _ := req.GetArguments()["description"].(string) description, _ := req.GetArguments()["description"].(string)
@@ -275,11 +265,11 @@ func DeleteOrgActionSecretFn(ctx context.Context, req mcp.CallToolRequest) (*mcp
log.Debugf("Called DeleteOrgActionSecretFn") log.Debugf("Called DeleteOrgActionSecretFn")
org, ok := req.GetArguments()["org"].(string) org, ok := req.GetArguments()["org"].(string)
if !ok || org == "" { if !ok || org == "" {
return to.ErrorResult(fmt.Errorf("org is required")) return to.ErrorResult(errors.New("org is required"))
} }
secretName, ok := req.GetArguments()["secretName"].(string) secretName, ok := req.GetArguments()["secretName"].(string)
if !ok || secretName == "" { if !ok || secretName == "" {
return to.ErrorResult(fmt.Errorf("secretName is required")) return to.ErrorResult(errors.New("secretName is required"))
} }
escapedOrg := url.PathEscape(org) escapedOrg := url.PathEscape(org)

View File

@@ -2,12 +2,15 @@ package actions
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"net/url" "net/url"
"strconv"
"gitea.com/gitea/gitea-mcp/pkg/gitea" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/gitea"
"gitea.com/gitea/gitea-mcp/pkg/log" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/log"
"gitea.com/gitea/gitea-mcp/pkg/to" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/params"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/to"
gitea_sdk "code.gitea.io/sdk/gitea" gitea_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/mcp"
@@ -21,11 +24,11 @@ const (
UpdateRepoActionVariableToolName = "update_repo_action_variable" UpdateRepoActionVariableToolName = "update_repo_action_variable"
DeleteRepoActionVariableToolName = "delete_repo_action_variable" DeleteRepoActionVariableToolName = "delete_repo_action_variable"
ListOrgActionVariablesToolName = "list_org_action_variables" ListOrgActionVariablesToolName = "list_org_action_variables"
GetOrgActionVariableToolName = "get_org_action_variable" GetOrgActionVariableToolName = "get_org_action_variable"
CreateOrgActionVariableToolName = "create_org_action_variable" CreateOrgActionVariableToolName = "create_org_action_variable"
UpdateOrgActionVariableToolName = "update_org_action_variable" UpdateOrgActionVariableToolName = "update_org_action_variable"
DeleteOrgActionVariableToolName = "delete_org_action_variable" DeleteOrgActionVariableToolName = "delete_org_action_variable"
) )
var ( var (
@@ -131,24 +134,18 @@ func ListRepoActionVariablesFn(ctx context.Context, req mcp.CallToolRequest) (*m
log.Debugf("Called ListRepoActionVariablesFn") log.Debugf("Called ListRepoActionVariablesFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok || owner == "" { if !ok || owner == "" {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok || repo == "" { if !ok || repo == "" {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
}
page, _ := req.GetArguments()["page"].(float64)
if page <= 0 {
page = 1
}
pageSize, _ := req.GetArguments()["pageSize"].(float64)
if pageSize <= 0 {
pageSize = 100
} }
page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
query := url.Values{} query := url.Values{}
query.Set("page", fmt.Sprintf("%d", int(page))) query.Set("page", strconv.Itoa(int(page)))
query.Set("limit", fmt.Sprintf("%d", int(pageSize))) query.Set("limit", strconv.Itoa(int(pageSize)))
var result any var result any
_, err := gitea.DoJSON(ctx, "GET", fmt.Sprintf("repos/%s/%s/actions/variables", url.PathEscape(owner), url.PathEscape(repo)), query, nil, &result) _, err := gitea.DoJSON(ctx, "GET", fmt.Sprintf("repos/%s/%s/actions/variables", url.PathEscape(owner), url.PathEscape(repo)), query, nil, &result)
@@ -162,15 +159,15 @@ func GetRepoActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (*mcp
log.Debugf("Called GetRepoActionVariableFn") log.Debugf("Called GetRepoActionVariableFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok || owner == "" { if !ok || owner == "" {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok || repo == "" { if !ok || repo == "" {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
name, ok := req.GetArguments()["name"].(string) name, ok := req.GetArguments()["name"].(string)
if !ok || name == "" { if !ok || name == "" {
return to.ErrorResult(fmt.Errorf("name is required")) return to.ErrorResult(errors.New("name is required"))
} }
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
@@ -188,19 +185,19 @@ func CreateRepoActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (*
log.Debugf("Called CreateRepoActionVariableFn") log.Debugf("Called CreateRepoActionVariableFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok || owner == "" { if !ok || owner == "" {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok || repo == "" { if !ok || repo == "" {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
name, ok := req.GetArguments()["name"].(string) name, ok := req.GetArguments()["name"].(string)
if !ok || name == "" { if !ok || name == "" {
return to.ErrorResult(fmt.Errorf("name is required")) return to.ErrorResult(errors.New("name is required"))
} }
value, ok := req.GetArguments()["value"].(string) value, ok := req.GetArguments()["value"].(string)
if !ok || value == "" { if !ok || value == "" {
return to.ErrorResult(fmt.Errorf("value is required")) return to.ErrorResult(errors.New("value is required"))
} }
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
@@ -218,19 +215,19 @@ func UpdateRepoActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (*
log.Debugf("Called UpdateRepoActionVariableFn") log.Debugf("Called UpdateRepoActionVariableFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok || owner == "" { if !ok || owner == "" {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok || repo == "" { if !ok || repo == "" {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
name, ok := req.GetArguments()["name"].(string) name, ok := req.GetArguments()["name"].(string)
if !ok || name == "" { if !ok || name == "" {
return to.ErrorResult(fmt.Errorf("name is required")) return to.ErrorResult(errors.New("name is required"))
} }
value, ok := req.GetArguments()["value"].(string) value, ok := req.GetArguments()["value"].(string)
if !ok || value == "" { if !ok || value == "" {
return to.ErrorResult(fmt.Errorf("value is required")) return to.ErrorResult(errors.New("value is required"))
} }
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
@@ -248,15 +245,15 @@ func DeleteRepoActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (*
log.Debugf("Called DeleteRepoActionVariableFn") log.Debugf("Called DeleteRepoActionVariableFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok || owner == "" { if !ok || owner == "" {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok || repo == "" { if !ok || repo == "" {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
name, ok := req.GetArguments()["name"].(string) name, ok := req.GetArguments()["name"].(string)
if !ok || name == "" { if !ok || name == "" {
return to.ErrorResult(fmt.Errorf("name is required")) return to.ErrorResult(errors.New("name is required"))
} }
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
@@ -274,16 +271,10 @@ func ListOrgActionVariablesFn(ctx context.Context, req mcp.CallToolRequest) (*mc
log.Debugf("Called ListOrgActionVariablesFn") log.Debugf("Called ListOrgActionVariablesFn")
org, ok := req.GetArguments()["org"].(string) org, ok := req.GetArguments()["org"].(string)
if !ok || org == "" { if !ok || org == "" {
return to.ErrorResult(fmt.Errorf("org is required")) return to.ErrorResult(errors.New("org is required"))
}
page, _ := req.GetArguments()["page"].(float64)
if page <= 0 {
page = 1
}
pageSize, _ := req.GetArguments()["pageSize"].(float64)
if pageSize <= 0 {
pageSize = 100
} }
page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
if err != nil { if err != nil {
@@ -302,11 +293,11 @@ func GetOrgActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.
log.Debugf("Called GetOrgActionVariableFn") log.Debugf("Called GetOrgActionVariableFn")
org, ok := req.GetArguments()["org"].(string) org, ok := req.GetArguments()["org"].(string)
if !ok || org == "" { if !ok || org == "" {
return to.ErrorResult(fmt.Errorf("org is required")) return to.ErrorResult(errors.New("org is required"))
} }
name, ok := req.GetArguments()["name"].(string) name, ok := req.GetArguments()["name"].(string)
if !ok || name == "" { if !ok || name == "" {
return to.ErrorResult(fmt.Errorf("name is required")) return to.ErrorResult(errors.New("name is required"))
} }
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
@@ -324,15 +315,15 @@ func CreateOrgActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (*m
log.Debugf("Called CreateOrgActionVariableFn") log.Debugf("Called CreateOrgActionVariableFn")
org, ok := req.GetArguments()["org"].(string) org, ok := req.GetArguments()["org"].(string)
if !ok || org == "" { if !ok || org == "" {
return to.ErrorResult(fmt.Errorf("org is required")) return to.ErrorResult(errors.New("org is required"))
} }
name, ok := req.GetArguments()["name"].(string) name, ok := req.GetArguments()["name"].(string)
if !ok || name == "" { if !ok || name == "" {
return to.ErrorResult(fmt.Errorf("name is required")) return to.ErrorResult(errors.New("name is required"))
} }
value, ok := req.GetArguments()["value"].(string) value, ok := req.GetArguments()["value"].(string)
if !ok || value == "" { if !ok || value == "" {
return to.ErrorResult(fmt.Errorf("value is required")) return to.ErrorResult(errors.New("value is required"))
} }
description, _ := req.GetArguments()["description"].(string) description, _ := req.GetArguments()["description"].(string)
@@ -355,15 +346,15 @@ func UpdateOrgActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (*m
log.Debugf("Called UpdateOrgActionVariableFn") log.Debugf("Called UpdateOrgActionVariableFn")
org, ok := req.GetArguments()["org"].(string) org, ok := req.GetArguments()["org"].(string)
if !ok || org == "" { if !ok || org == "" {
return to.ErrorResult(fmt.Errorf("org is required")) return to.ErrorResult(errors.New("org is required"))
} }
name, ok := req.GetArguments()["name"].(string) name, ok := req.GetArguments()["name"].(string)
if !ok || name == "" { if !ok || name == "" {
return to.ErrorResult(fmt.Errorf("name is required")) return to.ErrorResult(errors.New("name is required"))
} }
value, ok := req.GetArguments()["value"].(string) value, ok := req.GetArguments()["value"].(string)
if !ok || value == "" { if !ok || value == "" {
return to.ErrorResult(fmt.Errorf("value is required")) return to.ErrorResult(errors.New("value is required"))
} }
description, _ := req.GetArguments()["description"].(string) description, _ := req.GetArguments()["description"].(string)
@@ -385,11 +376,11 @@ func DeleteOrgActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (*m
log.Debugf("Called DeleteOrgActionVariableFn") log.Debugf("Called DeleteOrgActionVariableFn")
org, ok := req.GetArguments()["org"].(string) org, ok := req.GetArguments()["org"].(string)
if !ok || org == "" { if !ok || org == "" {
return to.ErrorResult(fmt.Errorf("org is required")) return to.ErrorResult(errors.New("org is required"))
} }
name, ok := req.GetArguments()["name"].(string) name, ok := req.GetArguments()["name"].(string)
if !ok || name == "" { if !ok || name == "" {
return to.ErrorResult(fmt.Errorf("name is required")) return to.ErrorResult(errors.New("name is required"))
} }
_, err := gitea.DoJSON(ctx, "DELETE", fmt.Sprintf("orgs/%s/actions/variables/%s", url.PathEscape(org), url.PathEscape(name)), nil, nil, nil) _, err := gitea.DoJSON(ctx, "DELETE", fmt.Sprintf("orgs/%s/actions/variables/%s", url.PathEscape(org), url.PathEscape(name)), nil, nil, nil)
@@ -398,5 +389,3 @@ func DeleteOrgActionVariableFn(ctx context.Context, req mcp.CallToolRequest) (*m
} }
return to.TextResult(map[string]any{"message": "variable deleted"}) return to.TextResult(map[string]any{"message": "variable deleted"})
} }

805
operation/admin/admin.go Normal file
View File

@@ -0,0 +1,805 @@
package admin
import (
"context"
"errors"
"fmt"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/gitea"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/log"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/params"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/to"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/tool"
gitea_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
var Tool = tool.New()
const (
AdminListUsersToolName = "admin_list_users"
AdminCreateUserToolName = "admin_create_user"
AdminEditUserToolName = "admin_edit_user"
AdminDeleteUserToolName = "admin_delete_user"
AdminRenameUserToolName = "admin_rename_user"
AdminListOrgsToolName = "admin_list_orgs"
AdminCreateOrgToolName = "admin_create_org"
AdminCreateRepoToolName = "admin_create_repo"
AdminListHooksToolName = "admin_list_hooks"
AdminGetHookToolName = "admin_get_hook"
AdminCreateHookToolName = "admin_create_hook"
AdminEditHookToolName = "admin_edit_hook"
AdminDeleteHookToolName = "admin_delete_hook"
ListCronTasksToolName = "admin_list_cron_tasks"
RunCronTaskToolName = "admin_run_cron_task"
ListUnadoptedReposToolName = "admin_list_unadopted_repos"
AdoptUnadoptedRepoToolName = "admin_adopt_repo"
DeleteUnadoptedRepoToolName = "admin_delete_unadopted_repo"
AdminListEmailsToolName = "admin_list_emails"
AdminSearchEmailsToolName = "admin_search_emails"
ListUserBadgesToolName = "admin_list_user_badges"
AddUserBadgesToolName = "admin_add_user_badges"
DeleteUserBadgeToolName = "admin_delete_user_badge"
)
var (
AdminListUsersTool = mcp.NewTool(
AdminListUsersToolName,
mcp.WithDescription("Admin: List all users"),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)),
)
AdminCreateUserTool = mcp.NewTool(
AdminCreateUserToolName,
mcp.WithDescription("Admin: Create a new user"),
mcp.WithString("username", mcp.Required(), mcp.Description("username")),
mcp.WithString("email", mcp.Required(), mcp.Description("email address")),
mcp.WithString("password", mcp.Required(), mcp.Description("password")),
mcp.WithString("full_name", mcp.Description("full name")),
mcp.WithBoolean("must_change_password", mcp.Description("require password change on first login (default: true)")),
mcp.WithBoolean("send_notify", mcp.Description("send notification email")),
)
AdminEditUserTool = mcp.NewTool(
AdminEditUserToolName,
mcp.WithDescription("Admin: Edit a user account"),
mcp.WithString("username", mcp.Required(), mcp.Description("username to edit")),
mcp.WithString("email", mcp.Description("new email")),
mcp.WithString("password", mcp.Description("new password")),
mcp.WithString("full_name", mcp.Description("new full name")),
mcp.WithBoolean("must_change_password", mcp.Description("require password change")),
mcp.WithBoolean("admin", mcp.Description("set admin status")),
mcp.WithBoolean("active", mcp.Description("set active status")),
mcp.WithBoolean("prohibit_login", mcp.Description("prohibit login")),
mcp.WithBoolean("restricted", mcp.Description("set restricted status")),
mcp.WithBoolean("allow_create_organization", mcp.Description("allow creating organizations")),
)
AdminDeleteUserTool = mcp.NewTool(
AdminDeleteUserToolName,
mcp.WithDescription("Admin: Delete a user account"),
mcp.WithString("username", mcp.Required(), mcp.Description("username to delete")),
)
AdminRenameUserTool = mcp.NewTool(
AdminRenameUserToolName,
mcp.WithDescription("Admin: Rename a user"),
mcp.WithString("username", mcp.Required(), mcp.Description("current username")),
mcp.WithString("new_username", mcp.Required(), mcp.Description("new username")),
)
AdminListOrgsTool = mcp.NewTool(
AdminListOrgsToolName,
mcp.WithDescription("Admin: List all organizations"),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)),
)
AdminCreateOrgTool = mcp.NewTool(
AdminCreateOrgToolName,
mcp.WithDescription("Admin: Create an organization for a user"),
mcp.WithString("username", mcp.Required(), mcp.Description("owner username")),
mcp.WithString("name", mcp.Required(), mcp.Description("organization name")),
mcp.WithString("full_name", mcp.Description("organization full name")),
mcp.WithString("description", mcp.Description("organization description")),
mcp.WithString("visibility", mcp.Description("visibility: public, limited, private (default: public)")),
)
AdminCreateRepoTool = mcp.NewTool(
AdminCreateRepoToolName,
mcp.WithDescription("Admin: Create a repository for a user"),
mcp.WithString("username", mcp.Required(), mcp.Description("owner username")),
mcp.WithString("name", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("description", mcp.Description("repository description")),
mcp.WithBoolean("private", mcp.Description("make repository private")),
mcp.WithBoolean("auto_init", mcp.Description("auto-initialize with README")),
)
AdminListHooksTool = mcp.NewTool(
AdminListHooksToolName,
mcp.WithDescription("Admin: List system webhooks"),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)),
)
AdminGetHookTool = mcp.NewTool(
AdminGetHookToolName,
mcp.WithDescription("Admin: Get a system webhook by ID"),
mcp.WithNumber("id", mcp.Required(), mcp.Description("webhook ID")),
)
AdminCreateHookTool = mcp.NewTool(
AdminCreateHookToolName,
mcp.WithDescription("Admin: Create a system webhook"),
mcp.WithString("type", mcp.Required(), mcp.Description("hook type: gitea, gogs, slack, discord, dingtalk, telegram, msteams, feishu, wechatwork, packagist")),
mcp.WithString("url", mcp.Required(), mcp.Description("target URL")),
mcp.WithString("content_type", mcp.Description("content type: json or form (default: json)")),
mcp.WithString("secret", mcp.Description("webhook secret")),
mcp.WithBoolean("active", mcp.Description("whether the webhook is active (default: true)")),
)
AdminEditHookTool = mcp.NewTool(
AdminEditHookToolName,
mcp.WithDescription("Admin: Edit a system webhook"),
mcp.WithNumber("id", mcp.Required(), mcp.Description("webhook ID")),
mcp.WithString("url", mcp.Description("target URL")),
mcp.WithString("content_type", mcp.Description("content type: json or form")),
mcp.WithString("secret", mcp.Description("webhook secret")),
mcp.WithBoolean("active", mcp.Description("whether the webhook is active")),
)
AdminDeleteHookTool = mcp.NewTool(
AdminDeleteHookToolName,
mcp.WithDescription("Admin: Delete a system webhook"),
mcp.WithNumber("id", mcp.Required(), mcp.Description("webhook ID")),
)
ListCronTasksTool = mcp.NewTool(
ListCronTasksToolName,
mcp.WithDescription("Admin: List cron tasks"),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)),
)
RunCronTaskTool = mcp.NewTool(
RunCronTaskToolName,
mcp.WithDescription("Admin: Run a cron task manually"),
mcp.WithString("task", mcp.Required(), mcp.Description("cron task name")),
)
ListUnadoptedReposTool = mcp.NewTool(
ListUnadoptedReposToolName,
mcp.WithDescription("Admin: List unadopted repositories"),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)),
)
AdoptUnadoptedRepoTool = mcp.NewTool(
AdoptUnadoptedRepoToolName,
mcp.WithDescription("Admin: Adopt an unadopted repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
)
DeleteUnadoptedRepoTool = mcp.NewTool(
DeleteUnadoptedRepoToolName,
mcp.WithDescription("Admin: Delete an unadopted repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
)
AdminListEmailsTool = mcp.NewTool(
AdminListEmailsToolName,
mcp.WithDescription("Admin: List all emails"),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)),
)
AdminSearchEmailsTool = mcp.NewTool(
AdminSearchEmailsToolName,
mcp.WithDescription("Admin: Search emails"),
mcp.WithString("query", mcp.Required(), mcp.Description("search query")),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)),
)
ListUserBadgesTool = mcp.NewTool(
ListUserBadgesToolName,
mcp.WithDescription("Admin: List badges for a user"),
mcp.WithString("username", mcp.Required(), mcp.Description("username")),
)
AddUserBadgesTool = mcp.NewTool(
AddUserBadgesToolName,
mcp.WithDescription("Admin: Add badges to a user"),
mcp.WithString("username", mcp.Required(), mcp.Description("username")),
mcp.WithString("slugs", mcp.Required(), mcp.Description("comma-separated badge slugs to add")),
)
DeleteUserBadgeTool = mcp.NewTool(
DeleteUserBadgeToolName,
mcp.WithDescription("Admin: Remove badges from a user"),
mcp.WithString("username", mcp.Required(), mcp.Description("username")),
mcp.WithString("slugs", mcp.Required(), mcp.Description("comma-separated badge slugs to remove")),
)
)
func init() {
Tool.RegisterRead(server.ServerTool{Tool: AdminListUsersTool, Handler: AdminListUsersFn})
Tool.RegisterRead(server.ServerTool{Tool: AdminListOrgsTool, Handler: AdminListOrgsFn})
Tool.RegisterRead(server.ServerTool{Tool: AdminListHooksTool, Handler: AdminListHooksFn})
Tool.RegisterRead(server.ServerTool{Tool: AdminGetHookTool, Handler: AdminGetHookFn})
Tool.RegisterRead(server.ServerTool{Tool: ListCronTasksTool, Handler: ListCronTasksFn})
Tool.RegisterRead(server.ServerTool{Tool: ListUnadoptedReposTool, Handler: ListUnadoptedReposFn})
Tool.RegisterRead(server.ServerTool{Tool: AdminListEmailsTool, Handler: AdminListEmailsFn})
Tool.RegisterRead(server.ServerTool{Tool: AdminSearchEmailsTool, Handler: AdminSearchEmailsFn})
Tool.RegisterRead(server.ServerTool{Tool: ListUserBadgesTool, Handler: ListUserBadgesFn})
Tool.RegisterWrite(server.ServerTool{Tool: AdminCreateUserTool, Handler: AdminCreateUserFn})
Tool.RegisterWrite(server.ServerTool{Tool: AdminEditUserTool, Handler: AdminEditUserFn})
Tool.RegisterWrite(server.ServerTool{Tool: AdminDeleteUserTool, Handler: AdminDeleteUserFn})
Tool.RegisterWrite(server.ServerTool{Tool: AdminRenameUserTool, Handler: AdminRenameUserFn})
Tool.RegisterWrite(server.ServerTool{Tool: AdminCreateOrgTool, Handler: AdminCreateOrgFn})
Tool.RegisterWrite(server.ServerTool{Tool: AdminCreateRepoTool, Handler: AdminCreateRepoFn})
Tool.RegisterWrite(server.ServerTool{Tool: AdminCreateHookTool, Handler: AdminCreateHookFn})
Tool.RegisterWrite(server.ServerTool{Tool: AdminEditHookTool, Handler: AdminEditHookFn})
Tool.RegisterWrite(server.ServerTool{Tool: AdminDeleteHookTool, Handler: AdminDeleteHookFn})
Tool.RegisterWrite(server.ServerTool{Tool: RunCronTaskTool, Handler: RunCronTaskFn})
Tool.RegisterWrite(server.ServerTool{Tool: AdoptUnadoptedRepoTool, Handler: AdoptUnadoptedRepoFn})
Tool.RegisterWrite(server.ServerTool{Tool: DeleteUnadoptedRepoTool, Handler: DeleteUnadoptedRepoFn})
Tool.RegisterWrite(server.ServerTool{Tool: AddUserBadgesTool, Handler: AddUserBadgesFn})
Tool.RegisterWrite(server.ServerTool{Tool: DeleteUserBadgeTool, Handler: DeleteUserBadgeFn})
}
func AdminListUsersFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called AdminListUsersFn")
page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
users, _, err := client.AdminListUsers(gitea_sdk.AdminListUsersOptions{
ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)},
})
if err != nil {
return to.ErrorResult(fmt.Errorf("admin list users err: %v", err))
}
return to.TextResult(users)
}
func AdminCreateUserFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called AdminCreateUserFn")
username, _ := req.GetArguments()["username"].(string)
email, _ := req.GetArguments()["email"].(string)
password, _ := req.GetArguments()["password"].(string)
if username == "" || email == "" || password == "" {
return to.ErrorResult(errors.New("username, email, and password are required"))
}
mustChange := true
if v, ok := req.GetArguments()["must_change_password"].(bool); ok {
mustChange = v
}
opt := gitea_sdk.CreateUserOption{
LoginName: username,
Username: username,
Email: email,
Password: password,
MustChangePassword: &mustChange,
}
if v, ok := req.GetArguments()["full_name"].(string); ok {
opt.FullName = v
}
if v, ok := req.GetArguments()["send_notify"].(bool); ok {
opt.SendNotify = v
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
user, _, err := client.AdminCreateUser(opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("admin create user err: %v", err))
}
return to.TextResult(user)
}
func AdminEditUserFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called AdminEditUserFn")
username, _ := req.GetArguments()["username"].(string)
if username == "" {
return to.ErrorResult(errors.New("username is required"))
}
opt := gitea_sdk.EditUserOption{}
if v, ok := req.GetArguments()["email"].(string); ok {
opt.Email = &v
}
if v, ok := req.GetArguments()["password"].(string); ok {
opt.Password = v
}
if v, ok := req.GetArguments()["full_name"].(string); ok {
opt.FullName = &v
}
if v, ok := req.GetArguments()["must_change_password"].(bool); ok {
opt.MustChangePassword = &v
}
if v, ok := req.GetArguments()["admin"].(bool); ok {
opt.Admin = &v
}
if v, ok := req.GetArguments()["active"].(bool); ok {
opt.Active = &v
}
if v, ok := req.GetArguments()["prohibit_login"].(bool); ok {
opt.ProhibitLogin = &v
}
if v, ok := req.GetArguments()["restricted"].(bool); ok {
opt.Restricted = &v
}
if v, ok := req.GetArguments()["allow_create_organization"].(bool); ok {
opt.AllowCreateOrganization = &v
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.AdminEditUser(username, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("admin edit user err: %v", err))
}
return to.TextResult(map[string]string{"status": "updated", "username": username})
}
func AdminDeleteUserFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called AdminDeleteUserFn")
username, _ := req.GetArguments()["username"].(string)
if username == "" {
return to.ErrorResult(errors.New("username is required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.AdminDeleteUser(username)
if err != nil {
return to.ErrorResult(fmt.Errorf("admin delete user err: %v", err))
}
return to.TextResult(map[string]string{"status": "deleted", "username": username})
}
func AdminRenameUserFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called AdminRenameUserFn")
username, _ := req.GetArguments()["username"].(string)
newUsername, _ := req.GetArguments()["new_username"].(string)
if username == "" || newUsername == "" {
return to.ErrorResult(errors.New("username and new_username are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.AdminRenameUser(username, gitea_sdk.RenameUserOption{NewUsername: newUsername})
if err != nil {
return to.ErrorResult(fmt.Errorf("admin rename user err: %v", err))
}
return to.TextResult(map[string]string{"status": "renamed", "old": username, "new": newUsername})
}
func AdminListOrgsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called AdminListOrgsFn")
page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
orgs, _, err := client.AdminListOrgs(gitea_sdk.AdminListOrgsOptions{
ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)},
})
if err != nil {
return to.ErrorResult(fmt.Errorf("admin list orgs err: %v", err))
}
return to.TextResult(orgs)
}
func AdminCreateOrgFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called AdminCreateOrgFn")
username, _ := req.GetArguments()["username"].(string)
name, _ := req.GetArguments()["name"].(string)
if username == "" || name == "" {
return to.ErrorResult(errors.New("username and name are required"))
}
opt := gitea_sdk.CreateOrgOption{
Name: name,
}
if v, ok := req.GetArguments()["full_name"].(string); ok {
opt.FullName = v
}
if v, ok := req.GetArguments()["description"].(string); ok {
opt.Description = v
}
if v, ok := req.GetArguments()["visibility"].(string); ok {
opt.Visibility = gitea_sdk.VisibleType(v)
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
org, _, err := client.AdminCreateOrg(username, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("admin create org err: %v", err))
}
return to.TextResult(org)
}
func AdminCreateRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called AdminCreateRepoFn")
username, _ := req.GetArguments()["username"].(string)
name, _ := req.GetArguments()["name"].(string)
if username == "" || name == "" {
return to.ErrorResult(errors.New("username and name are required"))
}
opt := gitea_sdk.CreateRepoOption{
Name: name,
}
if v, ok := req.GetArguments()["description"].(string); ok {
opt.Description = v
}
if v, ok := req.GetArguments()["private"].(bool); ok {
opt.Private = v
}
if v, ok := req.GetArguments()["auto_init"].(bool); ok {
opt.AutoInit = v
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
repo, _, err := client.AdminCreateRepo(username, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("admin create repo err: %v", err))
}
return to.TextResult(repo)
}
func AdminListHooksFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called AdminListHooksFn")
page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
hooks, _, err := client.ListAdminHooks(gitea_sdk.ListAdminHooksOptions{
ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)},
})
if err != nil {
return to.ErrorResult(fmt.Errorf("admin list hooks err: %v", err))
}
return to.TextResult(hooks)
}
func AdminGetHookFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called AdminGetHookFn")
id, err := params.GetIndex(req.GetArguments(), "id")
if err != nil {
return to.ErrorResult(err)
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
hook, _, err := client.GetAdminHook(id)
if err != nil {
return to.ErrorResult(fmt.Errorf("admin get hook err: %v", err))
}
return to.TextResult(hook)
}
func AdminCreateHookFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called AdminCreateHookFn")
hookType, _ := req.GetArguments()["type"].(string)
url, _ := req.GetArguments()["url"].(string)
if hookType == "" || url == "" {
return to.ErrorResult(errors.New("type and url are required"))
}
contentType := "json"
if v, ok := req.GetArguments()["content_type"].(string); ok {
contentType = v
}
config := map[string]string{
"url": url,
"content_type": contentType,
}
if v, ok := req.GetArguments()["secret"].(string); ok {
config["secret"] = v
}
opt := gitea_sdk.CreateHookOption{
Type: gitea_sdk.HookType(hookType),
Config: config,
Active: true,
}
if v, ok := req.GetArguments()["active"].(bool); ok {
opt.Active = v
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
hook, _, err := client.CreateAdminHook(opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("admin create hook err: %v", err))
}
return to.TextResult(hook)
}
func AdminEditHookFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called AdminEditHookFn")
id, err := params.GetIndex(req.GetArguments(), "id")
if err != nil {
return to.ErrorResult(err)
}
opt := gitea_sdk.EditHookOption{}
config := map[string]string{}
if v, ok := req.GetArguments()["url"].(string); ok {
config["url"] = v
}
if v, ok := req.GetArguments()["content_type"].(string); ok {
config["content_type"] = v
}
if v, ok := req.GetArguments()["secret"].(string); ok {
config["secret"] = v
}
if len(config) > 0 {
opt.Config = config
}
if v, ok := req.GetArguments()["active"].(bool); ok {
opt.Active = &v
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, _, err = client.EditAdminHook(id, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("admin edit hook err: %v", err))
}
return to.TextResult(map[string]string{"status": "updated"})
}
func AdminDeleteHookFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called AdminDeleteHookFn")
id, err := params.GetIndex(req.GetArguments(), "id")
if err != nil {
return to.ErrorResult(err)
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.DeleteAdminHook(id)
if err != nil {
return to.ErrorResult(fmt.Errorf("admin delete hook err: %v", err))
}
return to.TextResult(map[string]string{"status": "deleted"})
}
func ListCronTasksFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListCronTasksFn")
page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
tasks, _, err := client.ListCronTasks(gitea_sdk.ListCronTaskOptions{
ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)},
})
if err != nil {
return to.ErrorResult(fmt.Errorf("list cron tasks err: %v", err))
}
return to.TextResult(tasks)
}
func RunCronTaskFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called RunCronTaskFn")
task, _ := req.GetArguments()["task"].(string)
if task == "" {
return to.ErrorResult(errors.New("task is required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.RunCronTasks(task)
if err != nil {
return to.ErrorResult(fmt.Errorf("run cron task err: %v", err))
}
return to.TextResult(map[string]string{"status": "triggered", "task": task})
}
func ListUnadoptedReposFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListUnadoptedReposFn")
page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
repos, _, err := client.ListUnadoptedRepos(gitea_sdk.ListUnadoptedReposOptions{
ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)},
})
if err != nil {
return to.ErrorResult(fmt.Errorf("list unadopted repos err: %v", err))
}
return to.TextResult(repos)
}
func AdoptUnadoptedRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called AdoptUnadoptedRepoFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.AdoptUnadoptedRepo(owner, repo)
if err != nil {
return to.ErrorResult(fmt.Errorf("adopt repo err: %v", err))
}
return to.TextResult(map[string]string{"status": "adopted"})
}
func DeleteUnadoptedRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called DeleteUnadoptedRepoFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.DeleteUnadoptedRepo(owner, repo)
if err != nil {
return to.ErrorResult(fmt.Errorf("delete unadopted repo err: %v", err))
}
return to.TextResult(map[string]string{"status": "deleted"})
}
func AdminListEmailsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called AdminListEmailsFn")
page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
emails, _, err := client.ListAdminEmails(gitea_sdk.ListAdminEmailsOptions{
ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)},
})
if err != nil {
return to.ErrorResult(fmt.Errorf("admin list emails err: %v", err))
}
return to.TextResult(emails)
}
func AdminSearchEmailsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called AdminSearchEmailsFn")
query, _ := req.GetArguments()["query"].(string)
if query == "" {
return to.ErrorResult(errors.New("query is required"))
}
page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
emails, _, err := client.SearchAdminEmails(gitea_sdk.SearchAdminEmailsOptions{
ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)},
Query: query,
})
if err != nil {
return to.ErrorResult(fmt.Errorf("admin search emails err: %v", err))
}
return to.TextResult(emails)
}
func splitAndTrim(s string) []string {
var result []string
for _, part := range splitByComma(s) {
trimmed := trimSpace(part)
if trimmed != "" {
result = append(result, trimmed)
}
}
return result
}
func splitByComma(s string) []string {
var result []string
start := 0
for i := 0; i < len(s); i++ {
if s[i] == ',' {
result = append(result, s[start:i])
start = i + 1
}
}
result = append(result, s[start:])
return result
}
func trimSpace(s string) string {
start, end := 0, len(s)
for start < end && (s[start] == ' ' || s[start] == '\t') {
start++
}
for end > start && (s[end-1] == ' ' || s[end-1] == '\t') {
end--
}
return s[start:end]
}
func ListUserBadgesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListUserBadgesFn")
username, _ := req.GetArguments()["username"].(string)
if username == "" {
return to.ErrorResult(errors.New("username is required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
badges, _, err := client.ListUserBadges(username)
if err != nil {
return to.ErrorResult(fmt.Errorf("list user badges err: %v", err))
}
return to.TextResult(badges)
}
func AddUserBadgesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called AddUserBadgesFn")
username, _ := req.GetArguments()["username"].(string)
slugs, _ := req.GetArguments()["slugs"].(string)
if username == "" || slugs == "" {
return to.ErrorResult(errors.New("username and slugs are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.AddUserBadges(username, gitea_sdk.UserBadgeOption{
BadgeSlugs: splitAndTrim(slugs),
})
if err != nil {
return to.ErrorResult(fmt.Errorf("add user badges err: %v", err))
}
return to.TextResult(map[string]string{"status": "added"})
}
func DeleteUserBadgeFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called DeleteUserBadgeFn")
username, _ := req.GetArguments()["username"].(string)
slugs, _ := req.GetArguments()["slugs"].(string)
if username == "" || slugs == "" {
return to.ErrorResult(errors.New("username and slugs are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.DeleteUserBadge(username, gitea_sdk.UserBadgeOption{
BadgeSlugs: splitAndTrim(slugs),
})
if err != nil {
return to.ErrorResult(fmt.Errorf("delete user badge err: %v", err))
}
return to.TextResult(map[string]string{"status": "removed"})
}

View File

@@ -2,13 +2,14 @@ package issue
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"gitea.com/gitea/gitea-mcp/pkg/gitea" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/gitea"
"gitea.com/gitea/gitea-mcp/pkg/log" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/log"
"gitea.com/gitea/gitea-mcp/pkg/ptr" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/params"
"gitea.com/gitea/gitea-mcp/pkg/to" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/to"
"gitea.com/gitea/gitea-mcp/pkg/tool" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/tool"
gitea_sdk "code.gitea.io/sdk/gitea" gitea_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/mcp"
@@ -72,7 +73,7 @@ var (
mcp.WithNumber("index", mcp.Required(), mcp.Description("repository issue index")), mcp.WithNumber("index", mcp.Required(), mcp.Description("repository issue index")),
mcp.WithString("title", mcp.Description("issue title"), mcp.DefaultString("")), mcp.WithString("title", mcp.Description("issue title"), mcp.DefaultString("")),
mcp.WithString("body", mcp.Description("issue body content")), mcp.WithString("body", mcp.Description("issue body content")),
mcp.WithArray("assignees", mcp.Description("usernames to assign to this issue"), mcp.Items(map[string]interface{}{"type": "string"})), mcp.WithArray("assignees", mcp.Description("usernames to assign to this issue"), mcp.Items(map[string]any{"type": "string"})),
mcp.WithNumber("milestone", mcp.Description("milestone number")), mcp.WithNumber("milestone", mcp.Description("milestone number")),
mcp.WithString("state", mcp.Description("issue state, one of open, closed, all")), mcp.WithString("state", mcp.Description("issue state, one of open, closed, all")),
) )
@@ -130,23 +131,23 @@ func GetIssueByIndexFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallT
log.Debugf("Called GetIssueByIndexFn") log.Debugf("Called GetIssueByIndexFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
index, ok := req.GetArguments()["index"].(float64) index, err := params.GetIndex(req.GetArguments(), "index")
if !ok { if err != nil {
return to.ErrorResult(fmt.Errorf("index is required")) return to.ErrorResult(err)
} }
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
} }
issue, _, err := client.GetIssue(owner, repo, int64(index)) issue, _, err := client.GetIssue(owner, repo, index)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("get %v/%v/issue/%v err: %v", owner, repo, int64(index), err)) return to.ErrorResult(fmt.Errorf("get %v/%v/issue/%v err: %v", owner, repo, index, err))
} }
return to.TextResult(issue) return to.TextResult(issue)
@@ -156,24 +157,18 @@ func ListRepoIssuesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo
log.Debugf("Called ListIssuesFn") log.Debugf("Called ListIssuesFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
state, ok := req.GetArguments()["state"].(string) state, ok := req.GetArguments()["state"].(string)
if !ok { if !ok {
state = "all" state = "all"
} }
page, ok := req.GetArguments()["page"].(float64) page := params.GetOptionalInt(req.GetArguments(), "page", 1)
if !ok { pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
page = 1
}
pageSize, ok := req.GetArguments()["pageSize"].(float64)
if !ok {
pageSize = 100
}
opt := gitea_sdk.ListIssueOption{ opt := gitea_sdk.ListIssueOption{
State: gitea_sdk.StateType(state), State: gitea_sdk.StateType(state),
ListOptions: gitea_sdk.ListOptions{ ListOptions: gitea_sdk.ListOptions{
@@ -196,19 +191,19 @@ func CreateIssueFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolR
log.Debugf("Called CreateIssueFn") log.Debugf("Called CreateIssueFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
title, ok := req.GetArguments()["title"].(string) title, ok := req.GetArguments()["title"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("title is required")) return to.ErrorResult(errors.New("title is required"))
} }
body, ok := req.GetArguments()["body"].(string) body, ok := req.GetArguments()["body"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("body is required")) return to.ErrorResult(errors.New("body is required"))
} }
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
if err != nil { if err != nil {
@@ -229,19 +224,19 @@ func CreateIssueCommentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Ca
log.Debugf("Called CreateIssueCommentFn") log.Debugf("Called CreateIssueCommentFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
index, ok := req.GetArguments()["index"].(float64) index, err := params.GetIndex(req.GetArguments(), "index")
if !ok { if err != nil {
return to.ErrorResult(fmt.Errorf("index is required")) return to.ErrorResult(err)
} }
body, ok := req.GetArguments()["body"].(string) body, ok := req.GetArguments()["body"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("body is required")) return to.ErrorResult(errors.New("body is required"))
} }
opt := gitea_sdk.CreateIssueCommentOption{ opt := gitea_sdk.CreateIssueCommentOption{
Body: body, Body: body,
@@ -250,9 +245,9 @@ func CreateIssueCommentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Ca
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
} }
issueComment, _, err := client.CreateIssueComment(owner, repo, int64(index), opt) issueComment, _, err := client.CreateIssueComment(owner, repo, index, opt)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("create %v/%v/issue/%v/comment err: %v", owner, repo, int64(index), err)) return to.ErrorResult(fmt.Errorf("create %v/%v/issue/%v/comment err: %v", owner, repo, index, err))
} }
return to.TextResult(issueComment) return to.TextResult(issueComment)
@@ -262,15 +257,15 @@ func EditIssueFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRes
log.Debugf("Called EditIssueFn") log.Debugf("Called EditIssueFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
index, ok := req.GetArguments()["index"].(float64) index, err := params.GetIndex(req.GetArguments(), "index")
if !ok { if err != nil {
return to.ErrorResult(fmt.Errorf("index is required")) return to.ErrorResult(err)
} }
opt := gitea_sdk.EditIssueOption{} opt := gitea_sdk.EditIssueOption{}
@@ -281,11 +276,11 @@ func EditIssueFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRes
} }
body, ok := req.GetArguments()["body"].(string) body, ok := req.GetArguments()["body"].(string)
if ok { if ok {
opt.Body = ptr.To(body) opt.Body = new(body)
} }
var assignees []string var assignees []string
if assigneesArg, exists := req.GetArguments()["assignees"]; exists { if assigneesArg, exists := req.GetArguments()["assignees"]; exists {
if assigneesSlice, ok := assigneesArg.([]interface{}); ok { if assigneesSlice, ok := assigneesArg.([]any); ok {
for _, assignee := range assigneesSlice { for _, assignee := range assigneesSlice {
if assigneeStr, ok := assignee.(string); ok { if assigneeStr, ok := assignee.(string); ok {
assignees = append(assignees, assigneeStr) assignees = append(assignees, assigneeStr)
@@ -294,22 +289,23 @@ func EditIssueFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRes
} }
} }
opt.Assignees = assignees opt.Assignees = assignees
milestone, ok := req.GetArguments()["milestone"].(float64) if val, exists := req.GetArguments()["milestone"]; exists {
if ok { if milestone, ok := params.ToInt64(val); ok {
opt.Milestone = ptr.To(int64(milestone)) opt.Milestone = new(milestone)
}
} }
state, ok := req.GetArguments()["state"].(string) state, ok := req.GetArguments()["state"].(string)
if ok { if ok {
opt.State = ptr.To(gitea_sdk.StateType(state)) opt.State = new(gitea_sdk.StateType(state))
} }
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
} }
issue, _, err := client.EditIssue(owner, repo, int64(index), opt) issue, _, err := client.EditIssue(owner, repo, index, opt)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("edit %v/%v/issue/%v err: %v", owner, repo, int64(index), err)) return to.ErrorResult(fmt.Errorf("edit %v/%v/issue/%v err: %v", owner, repo, index, err))
} }
return to.TextResult(issue) return to.TextResult(issue)
@@ -319,19 +315,19 @@ func EditIssueCommentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call
log.Debugf("Called EditIssueCommentFn") log.Debugf("Called EditIssueCommentFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
commentID, ok := req.GetArguments()["commentID"].(float64) commentID, err := params.GetIndex(req.GetArguments(), "commentID")
if !ok { if err != nil {
return to.ErrorResult(fmt.Errorf("comment ID is required")) return to.ErrorResult(err)
} }
body, ok := req.GetArguments()["body"].(string) body, ok := req.GetArguments()["body"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("body is required")) return to.ErrorResult(errors.New("body is required"))
} }
opt := gitea_sdk.EditIssueCommentOption{ opt := gitea_sdk.EditIssueCommentOption{
Body: body, Body: body,
@@ -340,9 +336,9 @@ func EditIssueCommentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
} }
issueComment, _, err := client.EditIssueComment(owner, repo, int64(commentID), opt) issueComment, _, err := client.EditIssueComment(owner, repo, commentID, opt)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("edit %v/%v/issues/comments/%v err: %v", owner, repo, int64(commentID), err)) return to.ErrorResult(fmt.Errorf("edit %v/%v/issues/comments/%v err: %v", owner, repo, commentID, err))
} }
return to.TextResult(issueComment) return to.TextResult(issueComment)
@@ -352,24 +348,24 @@ func GetIssueCommentsByIndexFn(ctx context.Context, req mcp.CallToolRequest) (*m
log.Debugf("Called GetIssueCommentsByIndexFn") log.Debugf("Called GetIssueCommentsByIndexFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
index, ok := req.GetArguments()["index"].(float64) index, err := params.GetIndex(req.GetArguments(), "index")
if !ok { if err != nil {
return to.ErrorResult(fmt.Errorf("index is required")) return to.ErrorResult(err)
} }
opt := gitea_sdk.ListIssueCommentOptions{} opt := gitea_sdk.ListIssueCommentOptions{}
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
} }
issue, _, err := client.ListIssueComments(owner, repo, int64(index), opt) issue, _, err := client.ListIssueComments(owner, repo, index, opt)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("get %v/%v/issues/%v/comments err: %v", owner, repo, int64(index), err)) return to.ErrorResult(fmt.Errorf("get %v/%v/issues/%v/comments err: %v", owner, repo, index, err))
} }
return to.TextResult(issue) return to.TextResult(issue)

151
operation/issue/pin.go Normal file
View File

@@ -0,0 +1,151 @@
package issue
import (
"context"
"errors"
"fmt"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/gitea"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/log"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/params"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/to"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
const (
ListPinnedIssuesToolName = "list_pinned_issues"
PinIssueToolName = "pin_issue"
UnpinIssueToolName = "unpin_issue"
MoveIssuePinToolName = "move_issue_pin"
)
var (
ListPinnedIssuesTool = mcp.NewTool(
ListPinnedIssuesToolName,
mcp.WithDescription("List pinned issues in a repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
)
PinIssueTool = mcp.NewTool(
PinIssueToolName,
mcp.WithDescription("Pin an issue in a repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("index", mcp.Required(), mcp.Description("issue index")),
)
UnpinIssueTool = mcp.NewTool(
UnpinIssueToolName,
mcp.WithDescription("Unpin an issue from a repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("index", mcp.Required(), mcp.Description("issue index")),
)
MoveIssuePinTool = mcp.NewTool(
MoveIssuePinToolName,
mcp.WithDescription("Move a pinned issue to a new position"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("index", mcp.Required(), mcp.Description("issue index")),
mcp.WithNumber("position", mcp.Required(), mcp.Description("new position (1-based)")),
)
)
func init() {
Tool.RegisterRead(server.ServerTool{Tool: ListPinnedIssuesTool, Handler: ListPinnedIssuesFn})
Tool.RegisterWrite(server.ServerTool{Tool: PinIssueTool, Handler: PinIssueFn})
Tool.RegisterWrite(server.ServerTool{Tool: UnpinIssueTool, Handler: UnpinIssueFn})
Tool.RegisterWrite(server.ServerTool{Tool: MoveIssuePinTool, Handler: MoveIssuePinFn})
}
func ListPinnedIssuesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListPinnedIssuesFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
issues, _, err := client.ListRepoPinnedIssues(owner, repo)
if err != nil {
return to.ErrorResult(fmt.Errorf("list pinned issues err: %v", err))
}
return to.TextResult(issues)
}
func PinIssueFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called PinIssueFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
index, err := params.GetIndex(req.GetArguments(), "index")
if err != nil {
return to.ErrorResult(err)
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.PinIssue(owner, repo, index)
if err != nil {
return to.ErrorResult(fmt.Errorf("pin issue err: %v", err))
}
return to.TextResult(map[string]string{"status": "pinned"})
}
func UnpinIssueFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called UnpinIssueFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
index, err := params.GetIndex(req.GetArguments(), "index")
if err != nil {
return to.ErrorResult(err)
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.UnpinIssue(owner, repo, index)
if err != nil {
return to.ErrorResult(fmt.Errorf("unpin issue err: %v", err))
}
return to.TextResult(map[string]string{"status": "unpinned"})
}
func MoveIssuePinFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called MoveIssuePinFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
index, err := params.GetIndex(req.GetArguments(), "index")
if err != nil {
return to.ErrorResult(err)
}
position, ok := req.GetArguments()["position"].(float64)
if !ok {
return to.ErrorResult(errors.New("position is required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.MoveIssuePin(owner, repo, index, int64(position))
if err != nil {
return to.ErrorResult(fmt.Errorf("move issue pin err: %v", err))
}
return to.TextResult(map[string]any{"status": "moved", "position": int64(position)})
}

224
operation/issue/reaction.go Normal file
View File

@@ -0,0 +1,224 @@
package issue
import (
"context"
"errors"
"fmt"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/gitea"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/log"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/params"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/to"
gitea_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
const (
ListIssueReactionsToolName = "list_issue_reactions"
PostIssueReactionToolName = "post_issue_reaction"
DeleteIssueReactionToolName = "delete_issue_reaction"
GetIssueCommentReactionsToolName = "get_issue_comment_reactions"
PostIssueCommentReactionToolName = "post_issue_comment_reaction"
DeleteIssueCommentReactionToolName = "delete_issue_comment_reaction"
)
var (
ListIssueReactionsTool = mcp.NewTool(
ListIssueReactionsToolName,
mcp.WithDescription("List reactions on an issue"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("index", mcp.Required(), mcp.Description("issue index")),
)
PostIssueReactionTool = mcp.NewTool(
PostIssueReactionToolName,
mcp.WithDescription("Add a reaction to an issue"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("index", mcp.Required(), mcp.Description("issue index")),
mcp.WithString("reaction", mcp.Required(), mcp.Description("reaction emoji (e.g., +1, -1, laugh, confused, heart, hooray, rocket, eyes)")),
)
DeleteIssueReactionTool = mcp.NewTool(
DeleteIssueReactionToolName,
mcp.WithDescription("Remove a reaction from an issue"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("index", mcp.Required(), mcp.Description("issue index")),
mcp.WithString("reaction", mcp.Required(), mcp.Description("reaction emoji to remove")),
)
GetIssueCommentReactionsTool = mcp.NewTool(
GetIssueCommentReactionsToolName,
mcp.WithDescription("List reactions on an issue comment"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("comment_id", mcp.Required(), mcp.Description("comment ID")),
)
PostIssueCommentReactionTool = mcp.NewTool(
PostIssueCommentReactionToolName,
mcp.WithDescription("Add a reaction to an issue comment"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("comment_id", mcp.Required(), mcp.Description("comment ID")),
mcp.WithString("reaction", mcp.Required(), mcp.Description("reaction emoji")),
)
DeleteIssueCommentReactionTool = mcp.NewTool(
DeleteIssueCommentReactionToolName,
mcp.WithDescription("Remove a reaction from an issue comment"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("comment_id", mcp.Required(), mcp.Description("comment ID")),
mcp.WithString("reaction", mcp.Required(), mcp.Description("reaction emoji to remove")),
)
)
func init() {
Tool.RegisterRead(server.ServerTool{Tool: ListIssueReactionsTool, Handler: ListIssueReactionsFn})
Tool.RegisterRead(server.ServerTool{Tool: GetIssueCommentReactionsTool, Handler: GetIssueCommentReactionsFn})
Tool.RegisterWrite(server.ServerTool{Tool: PostIssueReactionTool, Handler: PostIssueReactionFn})
Tool.RegisterWrite(server.ServerTool{Tool: DeleteIssueReactionTool, Handler: DeleteIssueReactionFn})
Tool.RegisterWrite(server.ServerTool{Tool: PostIssueCommentReactionTool, Handler: PostIssueCommentReactionFn})
Tool.RegisterWrite(server.ServerTool{Tool: DeleteIssueCommentReactionTool, Handler: DeleteIssueCommentReactionFn})
}
func ListIssueReactionsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListIssueReactionsFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
index, err := params.GetIndex(req.GetArguments(), "index")
if err != nil {
return to.ErrorResult(err)
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
reactions, _, err := client.ListIssueReactions(owner, repo, index, gitea_sdk.ListIssueReactionsOptions{})
if err != nil {
return to.ErrorResult(fmt.Errorf("list issue reactions err: %v", err))
}
return to.TextResult(reactions)
}
func PostIssueReactionFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called PostIssueReactionFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
reaction, _ := req.GetArguments()["reaction"].(string)
if owner == "" || repo == "" || reaction == "" {
return to.ErrorResult(errors.New("owner, repo, and reaction are required"))
}
index, err := params.GetIndex(req.GetArguments(), "index")
if err != nil {
return to.ErrorResult(err)
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
r, _, err := client.PostIssueReaction(owner, repo, index, reaction)
if err != nil {
return to.ErrorResult(fmt.Errorf("post issue reaction err: %v", err))
}
return to.TextResult(r)
}
func DeleteIssueReactionFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called DeleteIssueReactionFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
reaction, _ := req.GetArguments()["reaction"].(string)
if owner == "" || repo == "" || reaction == "" {
return to.ErrorResult(errors.New("owner, repo, and reaction are required"))
}
index, err := params.GetIndex(req.GetArguments(), "index")
if err != nil {
return to.ErrorResult(err)
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.DeleteIssueReaction(owner, repo, index, reaction)
if err != nil {
return to.ErrorResult(fmt.Errorf("delete issue reaction err: %v", err))
}
return to.TextResult(map[string]string{"status": "removed", "reaction": reaction})
}
func GetIssueCommentReactionsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetIssueCommentReactionsFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
commentID, err := params.GetIndex(req.GetArguments(), "comment_id")
if err != nil {
return to.ErrorResult(err)
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
reactions, _, err := client.GetIssueCommentReactions(owner, repo, commentID)
if err != nil {
return to.ErrorResult(fmt.Errorf("get issue comment reactions err: %v", err))
}
return to.TextResult(reactions)
}
func PostIssueCommentReactionFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called PostIssueCommentReactionFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
reaction, _ := req.GetArguments()["reaction"].(string)
if owner == "" || repo == "" || reaction == "" {
return to.ErrorResult(errors.New("owner, repo, and reaction are required"))
}
commentID, err := params.GetIndex(req.GetArguments(), "comment_id")
if err != nil {
return to.ErrorResult(err)
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
r, _, err := client.PostIssueCommentReaction(owner, repo, commentID, reaction)
if err != nil {
return to.ErrorResult(fmt.Errorf("post issue comment reaction err: %v", err))
}
return to.TextResult(r)
}
func DeleteIssueCommentReactionFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called DeleteIssueCommentReactionFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
reaction, _ := req.GetArguments()["reaction"].(string)
if owner == "" || repo == "" || reaction == "" {
return to.ErrorResult(errors.New("owner, repo, and reaction are required"))
}
commentID, err := params.GetIndex(req.GetArguments(), "comment_id")
if err != nil {
return to.ErrorResult(err)
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.DeleteIssueCommentReaction(owner, repo, commentID, reaction)
if err != nil {
return to.ErrorResult(fmt.Errorf("delete issue comment reaction err: %v", err))
}
return to.TextResult(map[string]string{"status": "removed", "reaction": reaction})
}

View File

@@ -0,0 +1,152 @@
package issue
import (
"context"
"errors"
"fmt"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/gitea"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/log"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/params"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/to"
gitea_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
const (
ListIssueSubscribersToolName = "list_issue_subscribers"
CheckIssueSubscriptionToolName = "check_issue_subscription"
IssueSubscribeToolName = "issue_subscribe"
IssueUnSubscribeToolName = "issue_unsubscribe"
)
var (
ListIssueSubscribersTool = mcp.NewTool(
ListIssueSubscribersToolName,
mcp.WithDescription("List users subscribed to an issue"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("index", mcp.Required(), mcp.Description("issue index")),
)
CheckIssueSubscriptionTool = mcp.NewTool(
CheckIssueSubscriptionToolName,
mcp.WithDescription("Check if the authenticated user is subscribed to an issue"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("index", mcp.Required(), mcp.Description("issue index")),
)
IssueSubscribeTool = mcp.NewTool(
IssueSubscribeToolName,
mcp.WithDescription("Subscribe to an issue"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("index", mcp.Required(), mcp.Description("issue index")),
)
IssueUnSubscribeTool = mcp.NewTool(
IssueUnSubscribeToolName,
mcp.WithDescription("Unsubscribe from an issue"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("index", mcp.Required(), mcp.Description("issue index")),
)
)
func init() {
Tool.RegisterRead(server.ServerTool{Tool: ListIssueSubscribersTool, Handler: ListIssueSubscribersFn})
Tool.RegisterRead(server.ServerTool{Tool: CheckIssueSubscriptionTool, Handler: CheckIssueSubscriptionFn})
Tool.RegisterWrite(server.ServerTool{Tool: IssueSubscribeTool, Handler: IssueSubscribeFn})
Tool.RegisterWrite(server.ServerTool{Tool: IssueUnSubscribeTool, Handler: IssueUnSubscribeFn})
}
func ListIssueSubscribersFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListIssueSubscribersFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
index, err := params.GetIndex(req.GetArguments(), "index")
if err != nil {
return to.ErrorResult(err)
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
users, _, err := client.ListIssueSubscribers(owner, repo, index, gitea_sdk.ListIssueSubscribersOptions{})
if err != nil {
return to.ErrorResult(fmt.Errorf("list issue subscribers err: %v", err))
}
return to.TextResult(users)
}
func CheckIssueSubscriptionFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CheckIssueSubscriptionFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
index, err := params.GetIndex(req.GetArguments(), "index")
if err != nil {
return to.ErrorResult(err)
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
info, _, err := client.CheckIssueSubscription(owner, repo, index)
if err != nil {
return to.ErrorResult(fmt.Errorf("check issue subscription err: %v", err))
}
return to.TextResult(info)
}
func IssueSubscribeFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called IssueSubscribeFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
index, err := params.GetIndex(req.GetArguments(), "index")
if err != nil {
return to.ErrorResult(err)
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.IssueSubscribe(owner, repo, index)
if err != nil {
return to.ErrorResult(fmt.Errorf("issue subscribe err: %v", err))
}
return to.TextResult(map[string]string{"status": "subscribed"})
}
func IssueUnSubscribeFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called IssueUnSubscribeFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
index, err := params.GetIndex(req.GetArguments(), "index")
if err != nil {
return to.ErrorResult(err)
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.IssueUnSubscribe(owner, repo, index)
if err != nil {
return to.ErrorResult(fmt.Errorf("issue unsubscribe err: %v", err))
}
return to.TextResult(map[string]string{"status": "unsubscribed"})
}

View File

@@ -0,0 +1,89 @@
package issue
import (
"context"
"errors"
"fmt"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/gitea"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/log"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/params"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/to"
gitea_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
const (
ListIssueTimelineToolName = "list_issue_timeline"
GetIssueTemplatesToolName = "get_issue_templates"
)
var (
ListIssueTimelineTool = mcp.NewTool(
ListIssueTimelineToolName,
mcp.WithDescription("List timeline events for an issue (comments, labels, milestones, references, etc.)"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("index", mcp.Required(), mcp.Description("issue index")),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)),
)
GetIssueTemplatesTool = mcp.NewTool(
GetIssueTemplatesToolName,
mcp.WithDescription("Get issue templates for a repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
)
)
func init() {
Tool.RegisterRead(server.ServerTool{Tool: ListIssueTimelineTool, Handler: ListIssueTimelineFn})
Tool.RegisterRead(server.ServerTool{Tool: GetIssueTemplatesTool, Handler: GetIssueTemplatesFn})
}
func ListIssueTimelineFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListIssueTimelineFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
index, err := params.GetIndex(req.GetArguments(), "index")
if err != nil {
return to.ErrorResult(err)
}
page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
events, _, err := client.ListIssueTimeline(owner, repo, index, gitea_sdk.ListIssueCommentOptions{
ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)},
})
if err != nil {
return to.ErrorResult(fmt.Errorf("list issue timeline err: %v", err))
}
return to.TextResult(events)
}
func GetIssueTemplatesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetIssueTemplatesFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
templates, _, err := client.GetIssueTemplates(owner, repo)
if err != nil {
return to.ErrorResult(fmt.Errorf("get issue templates err: %v", err))
}
return to.TextResult(templates)
}

View File

@@ -2,13 +2,14 @@ package label
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"gitea.com/gitea/gitea-mcp/pkg/gitea" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/gitea"
"gitea.com/gitea/gitea-mcp/pkg/log" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/log"
"gitea.com/gitea/gitea-mcp/pkg/ptr" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/params"
"gitea.com/gitea/gitea-mcp/pkg/to" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/to"
"gitea.com/gitea/gitea-mcp/pkg/tool" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/tool"
gitea_sdk "code.gitea.io/sdk/gitea" gitea_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/mcp"
@@ -27,10 +28,10 @@ const (
ReplaceIssueLabelsToolName = "replace_issue_labels" ReplaceIssueLabelsToolName = "replace_issue_labels"
ClearIssueLabelsToolName = "clear_issue_labels" ClearIssueLabelsToolName = "clear_issue_labels"
RemoveIssueLabelToolName = "remove_issue_label" RemoveIssueLabelToolName = "remove_issue_label"
ListOrgLabelsToolName = "list_org_labels" ListOrgLabelsToolName = "list_org_labels"
CreateOrgLabelToolName = "create_org_label" CreateOrgLabelToolName = "create_org_label"
EditOrgLabelToolName = "edit_org_label" EditOrgLabelToolName = "edit_org_label"
DeleteOrgLabelToolName = "delete_org_label" DeleteOrgLabelToolName = "delete_org_label"
) )
var ( var (
@@ -86,7 +87,7 @@ var (
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("index", mcp.Required(), mcp.Description("issue index")), mcp.WithNumber("index", mcp.Required(), mcp.Description("issue index")),
mcp.WithArray("labels", mcp.Required(), mcp.Description("array of label IDs to add"), mcp.Items(map[string]interface{}{"type": "number"})), mcp.WithArray("labels", mcp.Required(), mcp.Description("array of label IDs to add"), mcp.Items(map[string]any{"type": "number"})),
) )
ReplaceIssueLabelsTool = mcp.NewTool( ReplaceIssueLabelsTool = mcp.NewTool(
@@ -95,7 +96,7 @@ var (
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("index", mcp.Required(), mcp.Description("issue index")), mcp.WithNumber("index", mcp.Required(), mcp.Description("issue index")),
mcp.WithArray("labels", mcp.Required(), mcp.Description("array of label IDs to replace with"), mcp.Items(map[string]interface{}{"type": "number"})), mcp.WithArray("labels", mcp.Required(), mcp.Description("array of label IDs to replace with"), mcp.Items(map[string]any{"type": "number"})),
) )
ClearIssueLabelsTool = mcp.NewTool( ClearIssueLabelsTool = mcp.NewTool(
@@ -115,42 +116,41 @@ var (
mcp.WithNumber("label_id", mcp.Required(), mcp.Description("label ID to remove")), mcp.WithNumber("label_id", mcp.Required(), mcp.Description("label ID to remove")),
) )
ListOrgLabelsTool = mcp.NewTool( ListOrgLabelsTool = mcp.NewTool(
ListOrgLabelsToolName, ListOrgLabelsToolName,
mcp.WithDescription("Lists labels defined at organization level"), mcp.WithDescription("Lists labels defined at organization level"),
mcp.WithString("org", mcp.Required(), mcp.Description("organization name")), mcp.WithString("org", mcp.Required(), mcp.Description("organization name")),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)), mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)), mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)),
) )
CreateOrgLabelTool = mcp.NewTool( CreateOrgLabelTool = mcp.NewTool(
CreateOrgLabelToolName, CreateOrgLabelToolName,
mcp.WithDescription("Creates a new label for an organization"), mcp.WithDescription("Creates a new label for an organization"),
mcp.WithString("org", mcp.Required(), mcp.Description("organization name")), mcp.WithString("org", mcp.Required(), mcp.Description("organization name")),
mcp.WithString("name", mcp.Required(), mcp.Description("label name")), mcp.WithString("name", mcp.Required(), mcp.Description("label name")),
mcp.WithString("color", mcp.Required(), mcp.Description("label color (hex code, e.g., #RRGGBB)")), mcp.WithString("color", mcp.Required(), mcp.Description("label color (hex code, e.g., #RRGGBB)")),
mcp.WithString("description", mcp.Description("label description")), mcp.WithString("description", mcp.Description("label description")),
mcp.WithBoolean("exclusive", mcp.Description("whether the label is exclusive"), mcp.DefaultBool(false)), mcp.WithBoolean("exclusive", mcp.Description("whether the label is exclusive"), mcp.DefaultBool(false)),
) )
EditOrgLabelTool = mcp.NewTool( EditOrgLabelTool = mcp.NewTool(
EditOrgLabelToolName, EditOrgLabelToolName,
mcp.WithDescription("Edits an existing organization label"), mcp.WithDescription("Edits an existing organization label"),
mcp.WithString("org", mcp.Required(), mcp.Description("organization name")), mcp.WithString("org", mcp.Required(), mcp.Description("organization name")),
mcp.WithNumber("id", mcp.Required(), mcp.Description("label ID")), mcp.WithNumber("id", mcp.Required(), mcp.Description("label ID")),
mcp.WithString("name", mcp.Description("new label name")), mcp.WithString("name", mcp.Description("new label name")),
mcp.WithString("color", mcp.Description("new label color (hex code, e.g., #RRGGBB)")), mcp.WithString("color", mcp.Description("new label color (hex code, e.g., #RRGGBB)")),
mcp.WithString("description", mcp.Description("new label description")), mcp.WithString("description", mcp.Description("new label description")),
mcp.WithBoolean("exclusive", mcp.Description("whether the label is exclusive")), mcp.WithBoolean("exclusive", mcp.Description("whether the label is exclusive")),
) )
DeleteOrgLabelTool = mcp.NewTool(
DeleteOrgLabelToolName,
mcp.WithDescription("Deletes an organization label by ID"),
mcp.WithString("org", mcp.Required(), mcp.Description("organization name")),
mcp.WithNumber("id", mcp.Required(), mcp.Description("label ID")),
)
DeleteOrgLabelTool = mcp.NewTool(
DeleteOrgLabelToolName,
mcp.WithDescription("Deletes an organization label by ID"),
mcp.WithString("org", mcp.Required(), mcp.Description("organization name")),
mcp.WithNumber("id", mcp.Required(), mcp.Description("label ID")),
)
) )
func init() { func init() {
@@ -190,42 +190,36 @@ func init() {
Tool: RemoveIssueLabelTool, Tool: RemoveIssueLabelTool,
Handler: RemoveIssueLabelFn, Handler: RemoveIssueLabelFn,
}) })
Tool.RegisterRead(server.ServerTool{ Tool.RegisterRead(server.ServerTool{
Tool: ListOrgLabelsTool, Tool: ListOrgLabelsTool,
Handler: ListOrgLabelsFn, Handler: ListOrgLabelsFn,
}) })
Tool.RegisterWrite(server.ServerTool{ Tool.RegisterWrite(server.ServerTool{
Tool: CreateOrgLabelTool, Tool: CreateOrgLabelTool,
Handler: CreateOrgLabelFn, Handler: CreateOrgLabelFn,
}) })
Tool.RegisterWrite(server.ServerTool{ Tool.RegisterWrite(server.ServerTool{
Tool: EditOrgLabelTool, Tool: EditOrgLabelTool,
Handler: EditOrgLabelFn, Handler: EditOrgLabelFn,
}) })
Tool.RegisterWrite(server.ServerTool{ Tool.RegisterWrite(server.ServerTool{
Tool: DeleteOrgLabelTool, Tool: DeleteOrgLabelTool,
Handler: DeleteOrgLabelFn, Handler: DeleteOrgLabelFn,
}) })
} }
func ListRepoLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func ListRepoLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListRepoLabelsFn") log.Debugf("Called ListRepoLabelsFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
}
page, ok := req.GetArguments()["page"].(float64)
if !ok {
page = 1
}
pageSize, ok := req.GetArguments()["pageSize"].(float64)
if !ok {
pageSize = 100
} }
page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
opt := gitea_sdk.ListLabelsOptions{ opt := gitea_sdk.ListLabelsOptions{
ListOptions: gitea_sdk.ListOptions{ ListOptions: gitea_sdk.ListOptions{
@@ -248,24 +242,24 @@ func GetRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTool
log.Debugf("Called GetRepoLabelFn") log.Debugf("Called GetRepoLabelFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
id, ok := req.GetArguments()["id"].(float64) id, err := params.GetIndex(req.GetArguments(), "id")
if !ok { if err != nil {
return to.ErrorResult(fmt.Errorf("label ID is required")) return to.ErrorResult(err)
} }
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
} }
label, _, err := client.GetRepoLabel(owner, repo, int64(id)) label, _, err := client.GetRepoLabel(owner, repo, id)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("get %v/%v/label/%v err: %v", owner, repo, int64(id), err)) return to.ErrorResult(fmt.Errorf("get %v/%v/label/%v err: %v", owner, repo, id, err))
} }
return to.TextResult(label) return to.TextResult(label)
} }
@@ -274,19 +268,19 @@ func CreateRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallT
log.Debugf("Called CreateRepoLabelFn") log.Debugf("Called CreateRepoLabelFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
name, ok := req.GetArguments()["name"].(string) name, ok := req.GetArguments()["name"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("name is required")) return to.ErrorResult(errors.New("name is required"))
} }
color, ok := req.GetArguments()["color"].(string) color, ok := req.GetArguments()["color"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("color is required")) return to.ErrorResult(errors.New("color is required"))
} }
description, _ := req.GetArguments()["description"].(string) // Optional description, _ := req.GetArguments()["description"].(string) // Optional
@@ -311,35 +305,35 @@ func EditRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToo
log.Debugf("Called EditRepoLabelFn") log.Debugf("Called EditRepoLabelFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
id, ok := req.GetArguments()["id"].(float64) id, err := params.GetIndex(req.GetArguments(), "id")
if !ok { if err != nil {
return to.ErrorResult(fmt.Errorf("label ID is required")) return to.ErrorResult(err)
} }
opt := gitea_sdk.EditLabelOption{} opt := gitea_sdk.EditLabelOption{}
if name, ok := req.GetArguments()["name"].(string); ok { if name, ok := req.GetArguments()["name"].(string); ok {
opt.Name = ptr.To(name) opt.Name = new(name)
} }
if color, ok := req.GetArguments()["color"].(string); ok { if color, ok := req.GetArguments()["color"].(string); ok {
opt.Color = ptr.To(color) opt.Color = new(color)
} }
if description, ok := req.GetArguments()["description"].(string); ok { if description, ok := req.GetArguments()["description"].(string); ok {
opt.Description = ptr.To(description) opt.Description = new(description)
} }
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
} }
label, _, err := client.EditLabel(owner, repo, int64(id), opt) label, _, err := client.EditLabel(owner, repo, id, opt)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("edit %v/%v/label/%v err: %v", owner, repo, int64(id), err)) return to.ErrorResult(fmt.Errorf("edit %v/%v/label/%v err: %v", owner, repo, id, err))
} }
return to.TextResult(label) return to.TextResult(label)
} }
@@ -348,24 +342,24 @@ func DeleteRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallT
log.Debugf("Called DeleteRepoLabelFn") log.Debugf("Called DeleteRepoLabelFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
id, ok := req.GetArguments()["id"].(float64) id, err := params.GetIndex(req.GetArguments(), "id")
if !ok { if err != nil {
return to.ErrorResult(fmt.Errorf("label ID is required")) return to.ErrorResult(err)
} }
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
} }
_, err = client.DeleteLabel(owner, repo, int64(id)) _, err = client.DeleteLabel(owner, repo, id)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("delete %v/%v/label/%v err: %v", owner, repo, int64(id), err)) return to.ErrorResult(fmt.Errorf("delete %v/%v/label/%v err: %v", owner, repo, id, err))
} }
return to.TextResult("Label deleted successfully") return to.TextResult("Label deleted successfully")
} }
@@ -374,26 +368,26 @@ func AddIssueLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo
log.Debugf("Called AddIssueLabelsFn") log.Debugf("Called AddIssueLabelsFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
index, ok := req.GetArguments()["index"].(float64) index, err := params.GetIndex(req.GetArguments(), "index")
if !ok { if err != nil {
return to.ErrorResult(fmt.Errorf("issue index is required")) return to.ErrorResult(err)
} }
labelsRaw, ok := req.GetArguments()["labels"].([]interface{}) labelsRaw, ok := req.GetArguments()["labels"].([]any)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("labels (array of IDs) is required")) return to.ErrorResult(errors.New("labels (array of IDs) is required"))
} }
var labels []int64 var labels []int64
for _, l := range labelsRaw { for _, l := range labelsRaw {
if labelID, ok := l.(float64); ok { if labelID, ok := params.ToInt64(l); ok {
labels = append(labels, int64(labelID)) labels = append(labels, labelID)
} else { } else {
return to.ErrorResult(fmt.Errorf("invalid label ID in labels array")) return to.ErrorResult(errors.New("invalid label ID in labels array"))
} }
} }
@@ -405,9 +399,9 @@ func AddIssueLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
} }
issueLabels, _, err := client.AddIssueLabels(owner, repo, int64(index), opt) issueLabels, _, err := client.AddIssueLabels(owner, repo, index, opt)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("add labels to %v/%v/issue/%v err: %v", owner, repo, int64(index), err)) return to.ErrorResult(fmt.Errorf("add labels to %v/%v/issue/%v err: %v", owner, repo, index, err))
} }
return to.TextResult(issueLabels) return to.TextResult(issueLabels)
} }
@@ -416,26 +410,26 @@ func ReplaceIssueLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Ca
log.Debugf("Called ReplaceIssueLabelsFn") log.Debugf("Called ReplaceIssueLabelsFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
index, ok := req.GetArguments()["index"].(float64) index, err := params.GetIndex(req.GetArguments(), "index")
if !ok { if err != nil {
return to.ErrorResult(fmt.Errorf("issue index is required")) return to.ErrorResult(err)
} }
labelsRaw, ok := req.GetArguments()["labels"].([]interface{}) labelsRaw, ok := req.GetArguments()["labels"].([]any)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("labels (array of IDs) is required")) return to.ErrorResult(errors.New("labels (array of IDs) is required"))
} }
var labels []int64 var labels []int64
for _, l := range labelsRaw { for _, l := range labelsRaw {
if labelID, ok := l.(float64); ok { if labelID, ok := params.ToInt64(l); ok {
labels = append(labels, int64(labelID)) labels = append(labels, labelID)
} else { } else {
return to.ErrorResult(fmt.Errorf("invalid label ID in labels array")) return to.ErrorResult(errors.New("invalid label ID in labels array"))
} }
} }
@@ -447,9 +441,9 @@ func ReplaceIssueLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Ca
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
} }
issueLabels, _, err := client.ReplaceIssueLabels(owner, repo, int64(index), opt) issueLabels, _, err := client.ReplaceIssueLabels(owner, repo, index, opt)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("replace labels on %v/%v/issue/%v err: %v", owner, repo, int64(index), err)) return to.ErrorResult(fmt.Errorf("replace labels on %v/%v/issue/%v err: %v", owner, repo, index, err))
} }
return to.TextResult(issueLabels) return to.TextResult(issueLabels)
} }
@@ -458,24 +452,24 @@ func ClearIssueLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call
log.Debugf("Called ClearIssueLabelsFn") log.Debugf("Called ClearIssueLabelsFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
index, ok := req.GetArguments()["index"].(float64) index, err := params.GetIndex(req.GetArguments(), "index")
if !ok { if err != nil {
return to.ErrorResult(fmt.Errorf("issue index is required")) return to.ErrorResult(err)
} }
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
} }
_, err = client.ClearIssueLabels(owner, repo, int64(index)) _, err = client.ClearIssueLabels(owner, repo, index)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("clear labels on %v/%v/issue/%v err: %v", owner, repo, int64(index), err)) return to.ErrorResult(fmt.Errorf("clear labels on %v/%v/issue/%v err: %v", owner, repo, index, err))
} }
return to.TextResult("Labels cleared successfully") return to.TextResult("Labels cleared successfully")
} }
@@ -484,153 +478,147 @@ func RemoveIssueLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call
log.Debugf("Called RemoveIssueLabelFn") log.Debugf("Called RemoveIssueLabelFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
index, ok := req.GetArguments()["index"].(float64) index, err := params.GetIndex(req.GetArguments(), "index")
if !ok { if err != nil {
return to.ErrorResult(fmt.Errorf("issue index is required")) return to.ErrorResult(err)
} }
labelID, ok := req.GetArguments()["label_id"].(float64) labelID, err := params.GetIndex(req.GetArguments(), "label_id")
if !ok { if err != nil {
return to.ErrorResult(fmt.Errorf("label ID is required")) return to.ErrorResult(err)
} }
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
} }
_, err = client.DeleteIssueLabel(owner, repo, int64(index), int64(labelID)) _, err = client.DeleteIssueLabel(owner, repo, index, labelID)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("remove label %v from %v/%v/issue/%v err: %v", int64(labelID), owner, repo, int64(index), err)) return to.ErrorResult(fmt.Errorf("remove label %v from %v/%v/issue/%v err: %v", labelID, owner, repo, index, err))
} }
return to.TextResult("Label removed successfully") return to.TextResult("Label removed successfully")
} }
func ListOrgLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func ListOrgLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListOrgLabelsFn") log.Debugf("Called ListOrgLabelsFn")
org, ok := req.GetArguments()["org"].(string) org, ok := req.GetArguments()["org"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("org is required")) return to.ErrorResult(errors.New("org is required"))
} }
page, ok := req.GetArguments()["page"].(float64) page := params.GetOptionalInt(req.GetArguments(), "page", 1)
if !ok { pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
page = 1
}
pageSize, ok := req.GetArguments()["pageSize"].(float64)
if !ok {
pageSize = 100
}
opt := gitea_sdk.ListOrgLabelsOptions{ opt := gitea_sdk.ListOrgLabelsOptions{
ListOptions: gitea_sdk.ListOptions{ ListOptions: gitea_sdk.ListOptions{
Page: int(page), Page: int(page),
PageSize: int(pageSize), PageSize: int(pageSize),
}, },
} }
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
} }
labels, _, err := client.ListOrgLabels(org, opt) labels, _, err := client.ListOrgLabels(org, opt)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("list %v/labels err: %v", org, err)) return to.ErrorResult(fmt.Errorf("list %v/labels err: %v", org, err))
} }
return to.TextResult(labels) return to.TextResult(labels)
} }
func CreateOrgLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func CreateOrgLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CreateOrgLabelFn") log.Debugf("Called CreateOrgLabelFn")
org, ok := req.GetArguments()["org"].(string) org, ok := req.GetArguments()["org"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("org is required")) return to.ErrorResult(errors.New("org is required"))
} }
name, ok := req.GetArguments()["name"].(string) name, ok := req.GetArguments()["name"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("name is required")) return to.ErrorResult(errors.New("name is required"))
} }
color, ok := req.GetArguments()["color"].(string) color, ok := req.GetArguments()["color"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("color is required")) return to.ErrorResult(errors.New("color is required"))
} }
description, _ := req.GetArguments()["description"].(string) description, _ := req.GetArguments()["description"].(string)
exclusive, _ := req.GetArguments()["exclusive"].(bool) exclusive, _ := req.GetArguments()["exclusive"].(bool)
opt := gitea_sdk.CreateOrgLabelOption{ opt := gitea_sdk.CreateOrgLabelOption{
Name: name, Name: name,
Color: color, Color: color,
Description: description, Description: description,
Exclusive: exclusive, Exclusive: exclusive,
} }
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
} }
label, _, err := client.CreateOrgLabel(org, opt) label, _, err := client.CreateOrgLabel(org, opt)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("create %v/labels err: %v", org, err)) return to.ErrorResult(fmt.Errorf("create %v/labels err: %v", org, err))
} }
return to.TextResult(label) return to.TextResult(label)
} }
func EditOrgLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func EditOrgLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called EditOrgLabelFn") log.Debugf("Called EditOrgLabelFn")
org, ok := req.GetArguments()["org"].(string) org, ok := req.GetArguments()["org"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("org is required")) return to.ErrorResult(errors.New("org is required"))
} }
id, ok := req.GetArguments()["id"].(float64) id, err := params.GetIndex(req.GetArguments(), "id")
if !ok { if err != nil {
return to.ErrorResult(fmt.Errorf("label ID is required")) return to.ErrorResult(err)
} }
opt := gitea_sdk.EditOrgLabelOption{} opt := gitea_sdk.EditOrgLabelOption{}
if name, ok := req.GetArguments()["name"].(string); ok { if name, ok := req.GetArguments()["name"].(string); ok {
opt.Name = ptr.To(name) opt.Name = new(name)
} }
if color, ok := req.GetArguments()["color"].(string); ok { if color, ok := req.GetArguments()["color"].(string); ok {
opt.Color = ptr.To(color) opt.Color = new(color)
} }
if description, ok := req.GetArguments()["description"].(string); ok { if description, ok := req.GetArguments()["description"].(string); ok {
opt.Description = ptr.To(description) opt.Description = new(description)
} }
if exclusive, ok := req.GetArguments()["exclusive"].(bool); ok { if exclusive, ok := req.GetArguments()["exclusive"].(bool); ok {
opt.Exclusive = ptr.To(exclusive) opt.Exclusive = new(exclusive)
} }
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
} }
label, _, err := client.EditOrgLabel(org, int64(id), opt) label, _, err := client.EditOrgLabel(org, id, opt)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("edit %v/labels/%v err: %v", org, int64(id), err)) return to.ErrorResult(fmt.Errorf("edit %v/labels/%v err: %v", org, id, err))
} }
return to.TextResult(label) return to.TextResult(label)
} }
func DeleteOrgLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func DeleteOrgLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called DeleteOrgLabelFn") log.Debugf("Called DeleteOrgLabelFn")
org, ok := req.GetArguments()["org"].(string) org, ok := req.GetArguments()["org"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("org is required")) return to.ErrorResult(errors.New("org is required"))
} }
id, ok := req.GetArguments()["id"].(float64) id, err := params.GetIndex(req.GetArguments(), "id")
if !ok { if err != nil {
return to.ErrorResult(fmt.Errorf("label ID is required")) return to.ErrorResult(err)
} }
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
} }
_, err = client.DeleteOrgLabel(org, int64(id)) _, err = client.DeleteOrgLabel(org, id)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("delete %v/labels/%v err: %v", org, int64(id), err)) return to.ErrorResult(fmt.Errorf("delete %v/labels/%v err: %v", org, id, err))
} }
return to.TextResult("Label deleted successfully") return to.TextResult("Label deleted successfully")
} }

View File

@@ -2,13 +2,14 @@ package milestone
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"gitea.com/gitea/gitea-mcp/pkg/gitea" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/gitea"
"gitea.com/gitea/gitea-mcp/pkg/log" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/log"
"gitea.com/gitea/gitea-mcp/pkg/ptr" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/params"
"gitea.com/gitea/gitea-mcp/pkg/to" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/to"
"gitea.com/gitea/gitea-mcp/pkg/tool" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/tool"
gitea_sdk "code.gitea.io/sdk/gitea" gitea_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/mcp"
@@ -103,23 +104,23 @@ func GetMilestoneFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTool
log.Debugf("Called GetMilestoneFn") log.Debugf("Called GetMilestoneFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
id, ok := req.GetArguments()["id"].(float64) id, err := params.GetIndex(req.GetArguments(), "id")
if !ok { if err != nil {
return to.ErrorResult(fmt.Errorf("id is required")) return to.ErrorResult(err)
} }
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
} }
milestone, _, err := client.GetMilestone(owner, repo, int64(id)) milestone, _, err := client.GetMilestone(owner, repo, id)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("get %v/%v/milestone/%v err: %v", owner, repo, int64(id), err)) return to.ErrorResult(fmt.Errorf("get %v/%v/milestone/%v err: %v", owner, repo, id, err))
} }
return to.TextResult(milestone) return to.TextResult(milestone)
@@ -129,11 +130,11 @@ func ListMilestonesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo
log.Debugf("Called ListMilestonesFn") log.Debugf("Called ListMilestonesFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
state, ok := req.GetArguments()["state"].(string) state, ok := req.GetArguments()["state"].(string)
if !ok { if !ok {
@@ -143,14 +144,8 @@ func ListMilestonesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo
if !ok { if !ok {
name = "" name = ""
} }
page, ok := req.GetArguments()["page"].(float64) page := params.GetOptionalInt(req.GetArguments(), "page", 1)
if !ok { pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
page = 1
}
pageSize, ok := req.GetArguments()["pageSize"].(float64)
if !ok {
pageSize = 100
}
opt := gitea_sdk.ListMilestoneOption{ opt := gitea_sdk.ListMilestoneOption{
State: gitea_sdk.StateType(state), State: gitea_sdk.StateType(state),
Name: name, Name: name,
@@ -174,15 +169,15 @@ func CreateMilestoneFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallT
log.Debugf("Called CreateMilestoneFn") log.Debugf("Called CreateMilestoneFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
title, ok := req.GetArguments()["title"].(string) title, ok := req.GetArguments()["title"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("title is required")) return to.ErrorResult(errors.New("title is required"))
} }
opt := gitea_sdk.CreateMilestoneOption{ opt := gitea_sdk.CreateMilestoneOption{
@@ -210,15 +205,15 @@ func EditMilestoneFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToo
log.Debugf("Called EditMilestoneFn") log.Debugf("Called EditMilestoneFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
id, ok := req.GetArguments()["id"].(float64) id, err := params.GetIndex(req.GetArguments(), "id")
if !ok { if err != nil {
return to.ErrorResult(fmt.Errorf("id is required")) return to.ErrorResult(err)
} }
opt := gitea_sdk.EditMilestoneOption{} opt := gitea_sdk.EditMilestoneOption{}
@@ -229,20 +224,20 @@ func EditMilestoneFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToo
} }
description, ok := req.GetArguments()["description"].(string) description, ok := req.GetArguments()["description"].(string)
if ok { if ok {
opt.Description = ptr.To(description) opt.Description = new(description)
} }
state, ok := req.GetArguments()["state"].(string) state, ok := req.GetArguments()["state"].(string)
if ok { if ok {
opt.State = ptr.To(gitea_sdk.StateType(state)) opt.State = new(gitea_sdk.StateType(state))
} }
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
} }
milestone, _, err := client.EditMilestone(owner, repo, int64(id), opt) milestone, _, err := client.EditMilestone(owner, repo, id, opt)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("edit %v/%v/milestone/%v err: %v", owner, repo, int64(id), err)) return to.ErrorResult(fmt.Errorf("edit %v/%v/milestone/%v err: %v", owner, repo, id, err))
} }
return to.TextResult(milestone) return to.TextResult(milestone)
@@ -252,23 +247,23 @@ func DeleteMilestoneFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallT
log.Debugf("Called DeleteMilestoneFn") log.Debugf("Called DeleteMilestoneFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
id, ok := req.GetArguments()["id"].(float64) id, err := params.GetIndex(req.GetArguments(), "id")
if !ok { if err != nil {
return to.ErrorResult(fmt.Errorf("id is required")) return to.ErrorResult(err)
} }
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
} }
_, err = client.DeleteMilestone(owner, repo, int64(id)) _, err = client.DeleteMilestone(owner, repo, id)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("delete %v/%v/milestone/%v err: %v", owner, repo, int64(id), err)) return to.ErrorResult(fmt.Errorf("delete %v/%v/milestone/%v err: %v", owner, repo, id, err))
} }
return to.TextResult("Milestone deleted successfully") return to.TextResult("Milestone deleted successfully")

View File

@@ -0,0 +1,325 @@
package miscellaneous
import (
"context"
"errors"
"fmt"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/gitea"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/log"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/to"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/tool"
gitea_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
var Tool = tool.New()
const (
GetServerVersionToolName = "get_server_version"
ListGitignoreTemplatesToolName = "list_gitignore_templates"
GetGitignoreTemplateToolName = "get_gitignore_template"
ListLabelTemplatesToolName = "list_label_templates"
GetLabelTemplateToolName = "get_label_template"
ListLicenseTemplatesToolName = "list_license_templates"
GetLicenseTemplateToolName = "get_license_template"
RenderMarkdownToolName = "render_markdown"
RenderMarkupToolName = "render_markup"
GetNodeInfoToolName = "get_node_info"
GetSigningKeyGPGToolName = "get_signing_key_gpg"
GetSigningKeySSHToolName = "get_signing_key_ssh"
)
var (
GetServerVersionTool = mcp.NewTool(
GetServerVersionToolName,
mcp.WithDescription("Get the Gitea server version"),
)
ListGitignoreTemplatesTool = mcp.NewTool(
ListGitignoreTemplatesToolName,
mcp.WithDescription("List available .gitignore templates"),
)
GetGitignoreTemplateTool = mcp.NewTool(
GetGitignoreTemplateToolName,
mcp.WithDescription("Get the content of a .gitignore template by name"),
mcp.WithString("name", mcp.Required(), mcp.Description("template name (e.g., Go, Python, Node)")),
)
ListLabelTemplatesTool = mcp.NewTool(
ListLabelTemplatesToolName,
mcp.WithDescription("List available label templates"),
)
GetLabelTemplateTool = mcp.NewTool(
GetLabelTemplateToolName,
mcp.WithDescription("Get the labels from a label template"),
mcp.WithString("name", mcp.Required(), mcp.Description("template name")),
)
ListLicenseTemplatesTool = mcp.NewTool(
ListLicenseTemplatesToolName,
mcp.WithDescription("List available license templates"),
)
GetLicenseTemplateTool = mcp.NewTool(
GetLicenseTemplateToolName,
mcp.WithDescription("Get the content of a license template"),
mcp.WithString("name", mcp.Required(), mcp.Description("license name (e.g., MIT, GPL-3.0, Apache-2.0)")),
)
RenderMarkdownTool = mcp.NewTool(
RenderMarkdownToolName,
mcp.WithDescription("Render markdown text to HTML"),
mcp.WithString("text", mcp.Required(), mcp.Description("markdown text to render")),
mcp.WithString("mode", mcp.Description("render mode: markdown, gfm, comment (default: markdown)")),
mcp.WithString("context", mcp.Description("context for relative links (owner/repo format)")),
mcp.WithBoolean("wiki", mcp.Description("treat text as wiki content")),
)
RenderMarkupTool = mcp.NewTool(
RenderMarkupToolName,
mcp.WithDescription("Render markup content to HTML (supports multiple markup languages)"),
mcp.WithString("text", mcp.Required(), mcp.Description("markup text to render")),
mcp.WithString("mode", mcp.Description("render mode")),
mcp.WithString("context", mcp.Description("context for relative links (owner/repo format)")),
mcp.WithString("filepath", mcp.Description("file path to determine markup language")),
mcp.WithBoolean("wiki", mcp.Description("treat text as wiki content")),
)
GetNodeInfoTool = mcp.NewTool(
GetNodeInfoToolName,
mcp.WithDescription("Get the NodeInfo of the Gitea instance (federation metadata)"),
)
GetSigningKeyGPGTool = mcp.NewTool(
GetSigningKeyGPGToolName,
mcp.WithDescription("Get the GPG signing key of the Gitea instance"),
)
GetSigningKeySSHTool = mcp.NewTool(
GetSigningKeySSHToolName,
mcp.WithDescription("Get the SSH signing key of the Gitea instance"),
)
)
func init() {
Tool.RegisterRead(server.ServerTool{Tool: GetServerVersionTool, Handler: GetServerVersionFn})
Tool.RegisterRead(server.ServerTool{Tool: ListGitignoreTemplatesTool, Handler: ListGitignoreTemplatesFn})
Tool.RegisterRead(server.ServerTool{Tool: GetGitignoreTemplateTool, Handler: GetGitignoreTemplateFn})
Tool.RegisterRead(server.ServerTool{Tool: ListLabelTemplatesTool, Handler: ListLabelTemplatesFn})
Tool.RegisterRead(server.ServerTool{Tool: GetLabelTemplateTool, Handler: GetLabelTemplateFn})
Tool.RegisterRead(server.ServerTool{Tool: ListLicenseTemplatesTool, Handler: ListLicenseTemplatesFn})
Tool.RegisterRead(server.ServerTool{Tool: GetLicenseTemplateTool, Handler: GetLicenseTemplateFn})
Tool.RegisterRead(server.ServerTool{Tool: GetNodeInfoTool, Handler: GetNodeInfoFn})
Tool.RegisterRead(server.ServerTool{Tool: GetSigningKeyGPGTool, Handler: GetSigningKeyGPGFn})
Tool.RegisterRead(server.ServerTool{Tool: GetSigningKeySSHTool, Handler: GetSigningKeySSHFn})
Tool.RegisterWrite(server.ServerTool{Tool: RenderMarkdownTool, Handler: RenderMarkdownFn})
Tool.RegisterWrite(server.ServerTool{Tool: RenderMarkupTool, Handler: RenderMarkupFn})
}
func GetServerVersionFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetServerVersionFn")
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
version, _, err := client.ServerVersion()
if err != nil {
return to.ErrorResult(fmt.Errorf("get server version err: %v", err))
}
return to.TextResult(map[string]string{"version": version})
}
func ListGitignoreTemplatesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListGitignoreTemplatesFn")
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
templates, _, err := client.ListGitignoresTemplates()
if err != nil {
return to.ErrorResult(fmt.Errorf("list gitignore templates err: %v", err))
}
return to.TextResult(templates)
}
func GetGitignoreTemplateFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetGitignoreTemplateFn")
name, _ := req.GetArguments()["name"].(string)
if name == "" {
return to.ErrorResult(errors.New("name is required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
info, _, err := client.GetGitignoreTemplateInfo(name)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitignore template err: %v", err))
}
return to.TextResult(info)
}
func ListLabelTemplatesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListLabelTemplatesFn")
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
templates, _, err := client.ListLabelTemplates()
if err != nil {
return to.ErrorResult(fmt.Errorf("list label templates err: %v", err))
}
return to.TextResult(templates)
}
func GetLabelTemplateFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetLabelTemplateFn")
name, _ := req.GetArguments()["name"].(string)
if name == "" {
return to.ErrorResult(errors.New("name is required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
labels, _, err := client.GetLabelTemplate(name)
if err != nil {
return to.ErrorResult(fmt.Errorf("get label template err: %v", err))
}
return to.TextResult(labels)
}
func ListLicenseTemplatesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListLicenseTemplatesFn")
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
templates, _, err := client.ListLicenseTemplates()
if err != nil {
return to.ErrorResult(fmt.Errorf("list license templates err: %v", err))
}
return to.TextResult(templates)
}
func GetLicenseTemplateFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetLicenseTemplateFn")
name, _ := req.GetArguments()["name"].(string)
if name == "" {
return to.ErrorResult(errors.New("name is required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
info, _, err := client.GetLicenseTemplateInfo(name)
if err != nil {
return to.ErrorResult(fmt.Errorf("get license template err: %v", err))
}
return to.TextResult(info)
}
func RenderMarkdownFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called RenderMarkdownFn")
text, _ := req.GetArguments()["text"].(string)
if text == "" {
return to.ErrorResult(errors.New("text is required"))
}
opt := gitea_sdk.MarkdownOption{
Text: text,
}
if v, ok := req.GetArguments()["mode"].(string); ok {
opt.Mode = v
}
if v, ok := req.GetArguments()["context"].(string); ok {
opt.Context = v
}
if v, ok := req.GetArguments()["wiki"].(bool); ok {
opt.Wiki = v
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
html, _, err := client.RenderMarkdown(opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("render markdown err: %v", err))
}
return to.TextResult(map[string]string{"html": html})
}
func RenderMarkupFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called RenderMarkupFn")
text, _ := req.GetArguments()["text"].(string)
if text == "" {
return to.ErrorResult(errors.New("text is required"))
}
opt := gitea_sdk.MarkupOption{
Text: text,
}
if v, ok := req.GetArguments()["mode"].(string); ok {
opt.Mode = v
}
if v, ok := req.GetArguments()["context"].(string); ok {
opt.Context = v
}
if v, ok := req.GetArguments()["filepath"].(string); ok {
opt.FilePath = v
}
if v, ok := req.GetArguments()["wiki"].(bool); ok {
opt.Wiki = v
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
html, _, err := client.RenderMarkup(opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("render markup err: %v", err))
}
return to.TextResult(map[string]string{"html": html})
}
func GetNodeInfoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetNodeInfoFn")
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
info, _, err := client.GetNodeInfo()
if err != nil {
return to.ErrorResult(fmt.Errorf("get node info err: %v", err))
}
return to.TextResult(info)
}
func GetSigningKeyGPGFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetSigningKeyGPGFn")
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
key, _, err := client.GetSigningKeyGPG()
if err != nil {
return to.ErrorResult(fmt.Errorf("get signing key GPG err: %v", err))
}
return to.TextResult(map[string]string{"gpg_key": key})
}
func GetSigningKeySSHFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetSigningKeySSHFn")
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
key, _, err := client.GetSigningKeySSH()
if err != nil {
return to.ErrorResult(fmt.Errorf("get signing key SSH err: %v", err))
}
return to.TextResult(map[string]string{"ssh_key": key})
}

View File

@@ -0,0 +1,219 @@
package notification
import (
"context"
"errors"
"fmt"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/gitea"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/log"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/params"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/to"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/tool"
gitea_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
var Tool = tool.New()
const (
ListNotificationsToolName = "list_notifications"
CheckNewNotificationsToolName = "check_new_notifications"
GetNotificationToolName = "get_notification"
ReadNotificationToolName = "read_notification"
ReadAllNotificationsToolName = "read_all_notifications"
ListRepoNotificationsToolName = "list_repo_notifications"
ReadRepoNotificationsToolName = "read_repo_notifications"
)
var (
ListNotificationsTool = mcp.NewTool(
ListNotificationsToolName,
mcp.WithDescription("List the authenticated user's notifications"),
mcp.WithString("status", mcp.Description("filter by status: read, unread, pinned")),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)),
)
CheckNewNotificationsTool = mcp.NewTool(
CheckNewNotificationsToolName,
mcp.WithDescription("Check if there are new notifications"),
)
GetNotificationTool = mcp.NewTool(
GetNotificationToolName,
mcp.WithDescription("Get a notification thread by ID"),
mcp.WithNumber("id", mcp.Required(), mcp.Description("notification thread ID")),
)
ReadNotificationTool = mcp.NewTool(
ReadNotificationToolName,
mcp.WithDescription("Mark a notification as read"),
mcp.WithNumber("id", mcp.Required(), mcp.Description("notification thread ID")),
)
ReadAllNotificationsTool = mcp.NewTool(
ReadAllNotificationsToolName,
mcp.WithDescription("Mark all notifications as read"),
)
ListRepoNotificationsTool = mcp.NewTool(
ListRepoNotificationsToolName,
mcp.WithDescription("List notifications for a repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("status", mcp.Description("filter by status: read, unread, pinned")),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)),
)
ReadRepoNotificationsTool = mcp.NewTool(
ReadRepoNotificationsToolName,
mcp.WithDescription("Mark all notifications in a repository as read"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
)
)
func init() {
Tool.RegisterRead(server.ServerTool{Tool: ListNotificationsTool, Handler: ListNotificationsFn})
Tool.RegisterRead(server.ServerTool{Tool: CheckNewNotificationsTool, Handler: CheckNewNotificationsFn})
Tool.RegisterRead(server.ServerTool{Tool: GetNotificationTool, Handler: GetNotificationFn})
Tool.RegisterRead(server.ServerTool{Tool: ListRepoNotificationsTool, Handler: ListRepoNotificationsFn})
Tool.RegisterWrite(server.ServerTool{Tool: ReadNotificationTool, Handler: ReadNotificationFn})
Tool.RegisterWrite(server.ServerTool{Tool: ReadAllNotificationsTool, Handler: ReadAllNotificationsFn})
Tool.RegisterWrite(server.ServerTool{Tool: ReadRepoNotificationsTool, Handler: ReadRepoNotificationsFn})
}
func ListNotificationsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListNotificationsFn")
page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
opt := gitea_sdk.ListNotificationOptions{
ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)},
}
if v, ok := req.GetArguments()["status"].(string); ok {
opt.Status = []gitea_sdk.NotifyStatus{gitea_sdk.NotifyStatus(v)}
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
notifications, _, err := client.ListNotifications(opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("list notifications err: %v", err))
}
return to.TextResult(notifications)
}
func CheckNewNotificationsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CheckNewNotificationsFn")
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
count, _, err := client.CheckNotifications()
if err != nil {
return to.ErrorResult(fmt.Errorf("check notifications err: %v", err))
}
return to.TextResult(map[string]int64{"new_notifications": count})
}
func GetNotificationFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetNotificationFn")
id, err := params.GetIndex(req.GetArguments(), "id")
if err != nil {
return to.ErrorResult(err)
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
notification, _, err := client.GetNotification(id)
if err != nil {
return to.ErrorResult(fmt.Errorf("get notification %d err: %v", id, err))
}
return to.TextResult(notification)
}
func ReadNotificationFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ReadNotificationFn")
id, err := params.GetIndex(req.GetArguments(), "id")
if err != nil {
return to.ErrorResult(err)
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
notification, _, err := client.ReadNotification(id)
if err != nil {
return to.ErrorResult(fmt.Errorf("read notification %d err: %v", id, err))
}
return to.TextResult(notification)
}
func ReadAllNotificationsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ReadAllNotificationsFn")
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
notifications, _, err := client.ReadNotifications(gitea_sdk.MarkNotificationOptions{})
if err != nil {
return to.ErrorResult(fmt.Errorf("read all notifications err: %v", err))
}
return to.TextResult(notifications)
}
func ListRepoNotificationsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListRepoNotificationsFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(errors.New("owner is required"))
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(errors.New("repo is required"))
}
page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
opt := gitea_sdk.ListNotificationOptions{
ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)},
}
if v, ok := req.GetArguments()["status"].(string); ok {
opt.Status = []gitea_sdk.NotifyStatus{gitea_sdk.NotifyStatus(v)}
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
notifications, _, err := client.ListRepoNotifications(owner, repo, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("list %s/%s notifications err: %v", owner, repo, err))
}
return to.TextResult(notifications)
}
func ReadRepoNotificationsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ReadRepoNotificationsFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(errors.New("owner is required"))
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(errors.New("repo is required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
notifications, _, err := client.ReadRepoNotifications(owner, repo, gitea_sdk.MarkNotificationOptions{})
if err != nil {
return to.ErrorResult(fmt.Errorf("read %s/%s notifications err: %v", owner, repo, err))
}
return to.TextResult(notifications)
}

View File

@@ -10,20 +10,26 @@ import (
"syscall" "syscall"
"time" "time"
"gitea.com/gitea/gitea-mcp/operation/issue" "git.lethalbits.com/lethalbits/gitea-mcp-extended/operation/actions"
"gitea.com/gitea/gitea-mcp/operation/label" "git.lethalbits.com/lethalbits/gitea-mcp-extended/operation/admin"
"gitea.com/gitea/gitea-mcp/operation/milestone" "git.lethalbits.com/lethalbits/gitea-mcp-extended/operation/issue"
"gitea.com/gitea/gitea-mcp/operation/timetracking" "git.lethalbits.com/lethalbits/gitea-mcp-extended/operation/label"
"gitea.com/gitea/gitea-mcp/operation/actions" "git.lethalbits.com/lethalbits/gitea-mcp-extended/operation/milestone"
"gitea.com/gitea/gitea-mcp/operation/pull" "git.lethalbits.com/lethalbits/gitea-mcp-extended/operation/miscellaneous"
"gitea.com/gitea/gitea-mcp/operation/repo" "git.lethalbits.com/lethalbits/gitea-mcp-extended/operation/notification"
"gitea.com/gitea/gitea-mcp/operation/search" "git.lethalbits.com/lethalbits/gitea-mcp-extended/operation/organization"
"gitea.com/gitea/gitea-mcp/operation/user" "git.lethalbits.com/lethalbits/gitea-mcp-extended/operation/packages"
"gitea.com/gitea/gitea-mcp/operation/version" "git.lethalbits.com/lethalbits/gitea-mcp-extended/operation/pull"
"gitea.com/gitea/gitea-mcp/operation/wiki" "git.lethalbits.com/lethalbits/gitea-mcp-extended/operation/repo"
mcpContext "gitea.com/gitea/gitea-mcp/pkg/context" "git.lethalbits.com/lethalbits/gitea-mcp-extended/operation/search"
"gitea.com/gitea/gitea-mcp/pkg/flag" "git.lethalbits.com/lethalbits/gitea-mcp-extended/operation/settings"
"gitea.com/gitea/gitea-mcp/pkg/log" "git.lethalbits.com/lethalbits/gitea-mcp-extended/operation/timetracking"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/operation/user"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/operation/version"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/operation/wiki"
mcpContext "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/context"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/flag"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/log"
"github.com/mark3labs/mcp-go/server" "github.com/mark3labs/mcp-go/server"
) )
@@ -64,23 +70,45 @@ func RegisterTool(s *server.MCPServer) {
// Time Tracking Tool // Time Tracking Tool
s.AddTools(timetracking.Tool.Tools()...) s.AddTools(timetracking.Tool.Tools()...)
// Organization Tool
s.AddTools(organization.Tool.Tools()...)
// Notification Tool
s.AddTools(notification.Tool.Tools()...)
// Settings Tool
s.AddTools(settings.Tool.Tools()...)
// Package Tool
s.AddTools(packages.Tool.Tools()...)
// Miscellaneous Tool
s.AddTools(miscellaneous.Tool.Tools()...)
// Admin Tool
s.AddTools(admin.Tool.Tools()...)
s.DeleteTools("") s.DeleteTools("")
} }
// parseBearerToken extracts the Bearer token from an Authorization header. // parseAuthToken extracts the token from an Authorization header.
// Supports "Bearer <token>" (case-insensitive per RFC 7235) and
// Gitea-style "token <token>" formats.
// Returns the token and true if valid, empty string and false otherwise. // Returns the token and true if valid, empty string and false otherwise.
func parseBearerToken(authHeader string) (string, bool) { func parseAuthToken(authHeader string) (string, bool) {
const bearerPrefix = "Bearer " if len(authHeader) > 7 && strings.EqualFold(authHeader[:7], "Bearer ") {
if len(authHeader) < len(bearerPrefix) || !strings.HasPrefix(authHeader, bearerPrefix) { token := strings.TrimSpace(authHeader[7:])
return "", false if token != "" {
return token, true
}
} }
if len(authHeader) > 6 && strings.EqualFold(authHeader[:6], "token ") {
token := strings.TrimSpace(authHeader[len(bearerPrefix):]) token := strings.TrimSpace(authHeader[6:])
if token == "" { if token != "" {
return "", false return token, true
}
} }
return "", false
return token, true
} }
func getContextWithToken(ctx context.Context, r *http.Request) context.Context { func getContextWithToken(ctx context.Context, r *http.Request) context.Context {
@@ -89,7 +117,7 @@ func getContextWithToken(ctx context.Context, r *http.Request) context.Context {
return ctx return ctx
} }
token, ok := parseBearerToken(authHeader) token, ok := parseAuthToken(authHeader)
if !ok { if !ok {
return ctx return ctx
} }

View File

@@ -4,7 +4,7 @@ import (
"testing" "testing"
) )
func TestParseBearerToken(t *testing.T) { func TestParseAuthToken(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
header string header string
@@ -12,23 +12,29 @@ func TestParseBearerToken(t *testing.T) {
wantOK bool wantOK bool
}{ }{
{ {
name: "valid token", name: "valid Bearer token",
header: "Bearer validtoken", header: "Bearer validtoken",
wantToken: "validtoken", wantToken: "validtoken",
wantOK: true, wantOK: true,
}, },
{
name: "lowercase bearer",
header: "bearer lowercase",
wantToken: "lowercase",
wantOK: true,
},
{
name: "uppercase BEARER",
header: "BEARER uppercase",
wantToken: "uppercase",
wantOK: true,
},
{ {
name: "token with spaces trimmed", name: "token with spaces trimmed",
header: "Bearer spacedToken ", header: "Bearer spacedToken ",
wantToken: "spacedToken", wantToken: "spacedToken",
wantOK: true, wantOK: true,
}, },
{
name: "lowercase bearer should fail",
header: "bearer lowercase",
wantToken: "",
wantOK: false,
},
{ {
name: "bearer with no token", name: "bearer with no token",
header: "Bearer ", header: "Bearer ",
@@ -47,6 +53,24 @@ func TestParseBearerToken(t *testing.T) {
wantToken: "", wantToken: "",
wantOK: false, wantOK: false,
}, },
{
name: "Gitea token format",
header: "token giteaapitoken",
wantToken: "giteaapitoken",
wantOK: true,
},
{
name: "Gitea Token format capitalized",
header: "Token giteaapitoken",
wantToken: "giteaapitoken",
wantOK: true,
},
{
name: "token with no value",
header: "token ",
wantToken: "",
wantOK: false,
},
{ {
name: "different auth type", name: "different auth type",
header: "Basic dXNlcjpwYXNz", header: "Basic dXNlcjpwYXNz",
@@ -60,7 +84,7 @@ func TestParseBearerToken(t *testing.T) {
wantOK: false, wantOK: false,
}, },
{ {
name: "token with internal spaces", name: "bearer token with internal spaces",
header: "Bearer token with spaces", header: "Bearer token with spaces",
wantToken: "token with spaces", wantToken: "token with spaces",
wantOK: true, wantOK: true,
@@ -69,12 +93,12 @@ func TestParseBearerToken(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
gotToken, gotOK := parseBearerToken(tt.header) gotToken, gotOK := parseAuthToken(tt.header)
if gotToken != tt.wantToken { if gotToken != tt.wantToken {
t.Errorf("parseBearerToken() token = %q, want %q", gotToken, tt.wantToken) t.Errorf("parseAuthToken() token = %q, want %q", gotToken, tt.wantToken)
} }
if gotOK != tt.wantOK { if gotOK != tt.wantOK {
t.Errorf("parseBearerToken() ok = %v, want %v", gotOK, tt.wantOK) t.Errorf("parseAuthToken() ok = %v, want %v", gotOK, tt.wantOK)
} }
}) })
} }

View File

@@ -0,0 +1,86 @@
package organization
import (
"context"
"errors"
"fmt"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/gitea"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/log"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/params"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/to"
gitea_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
const (
ListOrgActivityFeedsToolName = "list_org_activity_feeds"
ListTeamActivityFeedsToolName = "list_team_activity_feeds"
)
var (
ListOrgActivityFeedsTool = mcp.NewTool(
ListOrgActivityFeedsToolName,
mcp.WithDescription("List an organization's activity feeds"),
mcp.WithString("org", mcp.Required(), mcp.Description("organization name")),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)),
)
ListTeamActivityFeedsTool = mcp.NewTool(
ListTeamActivityFeedsToolName,
mcp.WithDescription("List a team's activity feeds"),
mcp.WithNumber("id", mcp.Required(), mcp.Description("team ID")),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)),
)
)
func init() {
Tool.RegisterRead(server.ServerTool{Tool: ListOrgActivityFeedsTool, Handler: ListOrgActivityFeedsFn})
Tool.RegisterRead(server.ServerTool{Tool: ListTeamActivityFeedsTool, Handler: ListTeamActivityFeedsFn})
}
func ListOrgActivityFeedsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListOrgActivityFeedsFn")
org, ok := req.GetArguments()["org"].(string)
if !ok {
return to.ErrorResult(errors.New("org is required"))
}
page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
feeds, _, err := client.ListOrgActivityFeeds(org, gitea_sdk.ListOrgActivityFeedsOptions{
ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)},
})
if err != nil {
return to.ErrorResult(fmt.Errorf("list org %s activity feeds err: %v", org, err))
}
return to.TextResult(feeds)
}
func ListTeamActivityFeedsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListTeamActivityFeedsFn")
id, err := params.GetIndex(req.GetArguments(), "id")
if err != nil {
return to.ErrorResult(err)
}
page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
feeds, _, err := client.ListTeamActivityFeeds(id, gitea_sdk.ListTeamActivityFeedsOptions{
ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)},
})
if err != nil {
return to.ErrorResult(fmt.Errorf("list team %d activity feeds err: %v", id, err))
}
return to.TextResult(feeds)
}

View File

@@ -0,0 +1,145 @@
package organization
import (
"context"
"errors"
"fmt"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/gitea"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/log"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/params"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/to"
gitea_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
const (
ListOrgBlocksToolName = "list_org_blocks"
CheckOrgBlockToolName = "check_org_block"
BlockOrgUserToolName = "block_org_user"
UnblockOrgUserToolName = "unblock_org_user"
)
var (
ListOrgBlocksTool = mcp.NewTool(
ListOrgBlocksToolName,
mcp.WithDescription("List users blocked by an organization"),
mcp.WithString("org", mcp.Required(), mcp.Description("organization name")),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)),
)
CheckOrgBlockTool = mcp.NewTool(
CheckOrgBlockToolName,
mcp.WithDescription("Check if a user is blocked by an organization"),
mcp.WithString("org", mcp.Required(), mcp.Description("organization name")),
mcp.WithString("username", mcp.Required(), mcp.Description("username to check")),
)
BlockOrgUserTool = mcp.NewTool(
BlockOrgUserToolName,
mcp.WithDescription("Block a user from an organization"),
mcp.WithString("org", mcp.Required(), mcp.Description("organization name")),
mcp.WithString("username", mcp.Required(), mcp.Description("username to block")),
)
UnblockOrgUserTool = mcp.NewTool(
UnblockOrgUserToolName,
mcp.WithDescription("Unblock a user from an organization"),
mcp.WithString("org", mcp.Required(), mcp.Description("organization name")),
mcp.WithString("username", mcp.Required(), mcp.Description("username to unblock")),
)
)
func init() {
Tool.RegisterRead(server.ServerTool{Tool: ListOrgBlocksTool, Handler: ListOrgBlocksFn})
Tool.RegisterRead(server.ServerTool{Tool: CheckOrgBlockTool, Handler: CheckOrgBlockFn})
Tool.RegisterWrite(server.ServerTool{Tool: BlockOrgUserTool, Handler: BlockOrgUserFn})
Tool.RegisterWrite(server.ServerTool{Tool: UnblockOrgUserTool, Handler: UnblockOrgUserFn})
}
func ListOrgBlocksFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListOrgBlocksFn")
org, ok := req.GetArguments()["org"].(string)
if !ok {
return to.ErrorResult(errors.New("org is required"))
}
page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
users, _, err := client.ListOrgBlocks(org, gitea_sdk.ListOrgBlocksOptions{
ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)},
})
if err != nil {
return to.ErrorResult(fmt.Errorf("list org %s blocks err: %v", org, err))
}
return to.TextResult(users)
}
func CheckOrgBlockFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CheckOrgBlockFn")
org, ok := req.GetArguments()["org"].(string)
if !ok {
return to.ErrorResult(errors.New("org is required"))
}
username, ok := req.GetArguments()["username"].(string)
if !ok {
return to.ErrorResult(errors.New("username is required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
isBlocked, _, err := client.CheckOrgBlock(org, username)
if err != nil {
return to.ErrorResult(fmt.Errorf("check org %s block for %s err: %v", org, username, err))
}
return to.TextResult(map[string]any{"org": org, "username": username, "is_blocked": isBlocked})
}
func BlockOrgUserFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called BlockOrgUserFn")
org, ok := req.GetArguments()["org"].(string)
if !ok {
return to.ErrorResult(errors.New("org is required"))
}
username, ok := req.GetArguments()["username"].(string)
if !ok {
return to.ErrorResult(errors.New("username is required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.BlockOrgUser(org, username)
if err != nil {
return to.ErrorResult(fmt.Errorf("block user %s from org %s err: %v", username, org, err))
}
return to.TextResult(map[string]string{"status": "blocked", "org": org, "username": username})
}
func UnblockOrgUserFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called UnblockOrgUserFn")
org, ok := req.GetArguments()["org"].(string)
if !ok {
return to.ErrorResult(errors.New("org is required"))
}
username, ok := req.GetArguments()["username"].(string)
if !ok {
return to.ErrorResult(errors.New("username is required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.UnblockOrgUser(org, username)
if err != nil {
return to.ErrorResult(fmt.Errorf("unblock user %s from org %s err: %v", username, org, err))
}
return to.TextResult(map[string]string{"status": "unblocked", "org": org, "username": username})
}

View File

@@ -0,0 +1,251 @@
package organization
import (
"context"
"errors"
"fmt"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/gitea"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/log"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/params"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/to"
gitea_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
const (
ListOrgHooksToolName = "list_org_hooks"
GetOrgHookToolName = "get_org_hook"
CreateOrgHookToolName = "create_org_hook"
EditOrgHookToolName = "edit_org_hook"
DeleteOrgHookToolName = "delete_org_hook"
)
var (
ListOrgHooksTool = mcp.NewTool(
ListOrgHooksToolName,
mcp.WithDescription("List an organization's webhooks"),
mcp.WithString("org", mcp.Required(), mcp.Description("organization name")),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)),
)
GetOrgHookTool = mcp.NewTool(
GetOrgHookToolName,
mcp.WithDescription("Get an organization webhook by ID"),
mcp.WithString("org", mcp.Required(), mcp.Description("organization name")),
mcp.WithNumber("id", mcp.Required(), mcp.Description("webhook ID")),
)
CreateOrgHookTool = mcp.NewTool(
CreateOrgHookToolName,
mcp.WithDescription("Create an organization webhook"),
mcp.WithString("org", mcp.Required(), mcp.Description("organization name")),
mcp.WithString("type", mcp.Required(), mcp.Description("hook type: gitea, slack, discord, dingtalk, telegram, msteams, feishu, matrix, wechatwork, packagist")),
mcp.WithString("url", mcp.Required(), mcp.Description("target URL for the webhook")),
mcp.WithString("content_type", mcp.Description("content type: json or form"), mcp.DefaultString("json")),
mcp.WithString("secret", mcp.Description("webhook secret")),
mcp.WithBoolean("active", mcp.Description("whether the webhook is active (default: true)")),
mcp.WithArray("events", mcp.Description("list of events to trigger on"), mcp.Items(map[string]any{"type": "string"})),
)
EditOrgHookTool = mcp.NewTool(
EditOrgHookToolName,
mcp.WithDescription("Edit an organization webhook"),
mcp.WithString("org", mcp.Required(), mcp.Description("organization name")),
mcp.WithNumber("id", mcp.Required(), mcp.Description("webhook ID")),
mcp.WithString("url", mcp.Description("target URL")),
mcp.WithString("content_type", mcp.Description("content type: json or form")),
mcp.WithString("secret", mcp.Description("webhook secret")),
mcp.WithBoolean("active", mcp.Description("whether the webhook is active")),
mcp.WithArray("events", mcp.Description("list of events to trigger on"), mcp.Items(map[string]any{"type": "string"})),
)
DeleteOrgHookTool = mcp.NewTool(
DeleteOrgHookToolName,
mcp.WithDescription("Delete an organization webhook"),
mcp.WithString("org", mcp.Required(), mcp.Description("organization name")),
mcp.WithNumber("id", mcp.Required(), mcp.Description("webhook ID")),
)
)
func init() {
Tool.RegisterRead(server.ServerTool{Tool: ListOrgHooksTool, Handler: ListOrgHooksFn})
Tool.RegisterRead(server.ServerTool{Tool: GetOrgHookTool, Handler: GetOrgHookFn})
Tool.RegisterWrite(server.ServerTool{Tool: CreateOrgHookTool, Handler: CreateOrgHookFn})
Tool.RegisterWrite(server.ServerTool{Tool: EditOrgHookTool, Handler: EditOrgHookFn})
Tool.RegisterWrite(server.ServerTool{Tool: DeleteOrgHookTool, Handler: DeleteOrgHookFn})
}
func ListOrgHooksFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListOrgHooksFn")
org, ok := req.GetArguments()["org"].(string)
if !ok {
return to.ErrorResult(errors.New("org is required"))
}
page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
hooks, _, err := client.ListOrgHooks(org, gitea_sdk.ListHooksOptions{
ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)},
})
if err != nil {
return to.ErrorResult(fmt.Errorf("list org %s hooks err: %v", org, err))
}
return to.TextResult(hooks)
}
func GetOrgHookFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetOrgHookFn")
org, ok := req.GetArguments()["org"].(string)
if !ok {
return to.ErrorResult(errors.New("org is required"))
}
id, err := params.GetIndex(req.GetArguments(), "id")
if err != nil {
return to.ErrorResult(err)
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
hook, _, err := client.GetOrgHook(org, id)
if err != nil {
return to.ErrorResult(fmt.Errorf("get org %s hook %d err: %v", org, id, err))
}
return to.TextResult(hook)
}
func CreateOrgHookFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CreateOrgHookFn")
org, ok := req.GetArguments()["org"].(string)
if !ok {
return to.ErrorResult(errors.New("org is required"))
}
hookType, ok := req.GetArguments()["type"].(string)
if !ok {
return to.ErrorResult(errors.New("type is required"))
}
hookURL, ok := req.GetArguments()["url"].(string)
if !ok {
return to.ErrorResult(errors.New("url is required"))
}
contentType := "json"
if v, ok := req.GetArguments()["content_type"].(string); ok {
contentType = v
}
config := map[string]string{
"url": hookURL,
"content_type": contentType,
}
if v, ok := req.GetArguments()["secret"].(string); ok {
config["secret"] = v
}
opt := gitea_sdk.CreateHookOption{
Type: gitea_sdk.HookType(hookType),
Config: config,
Active: true,
}
if v, ok := req.GetArguments()["active"].(bool); ok {
opt.Active = v
}
if eventsArg, exists := req.GetArguments()["events"]; exists {
if eventsSlice, ok := eventsArg.([]any); ok {
events := make([]string, 0, len(eventsSlice))
for _, e := range eventsSlice {
if s, ok := e.(string); ok {
events = append(events, s)
}
}
opt.Events = events
}
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
hook, _, err := client.CreateOrgHook(org, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("create org %s hook err: %v", org, err))
}
return to.TextResult(hook)
}
func EditOrgHookFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called EditOrgHookFn")
org, ok := req.GetArguments()["org"].(string)
if !ok {
return to.ErrorResult(errors.New("org is required"))
}
id, err := params.GetIndex(req.GetArguments(), "id")
if err != nil {
return to.ErrorResult(err)
}
opt := gitea_sdk.EditHookOption{}
config := map[string]string{}
if v, ok := req.GetArguments()["url"].(string); ok {
config["url"] = v
}
if v, ok := req.GetArguments()["content_type"].(string); ok {
config["content_type"] = v
}
if v, ok := req.GetArguments()["secret"].(string); ok {
config["secret"] = v
}
if len(config) > 0 {
opt.Config = config
}
if v, ok := req.GetArguments()["active"].(bool); ok {
opt.Active = &v
}
if eventsArg, exists := req.GetArguments()["events"]; exists {
if eventsSlice, ok := eventsArg.([]any); ok {
events := make([]string, 0, len(eventsSlice))
for _, e := range eventsSlice {
if s, ok := e.(string); ok {
events = append(events, s)
}
}
opt.Events = events
}
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.EditOrgHook(org, id, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("edit org %s hook %d err: %v", org, id, err))
}
hook, _, err := client.GetOrgHook(org, id)
if err != nil {
return to.ErrorResult(fmt.Errorf("get org %s hook %d after edit err: %v", org, id, err))
}
return to.TextResult(hook)
}
func DeleteOrgHookFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called DeleteOrgHookFn")
org, ok := req.GetArguments()["org"].(string)
if !ok {
return to.ErrorResult(errors.New("org is required"))
}
id, err := params.GetIndex(req.GetArguments(), "id")
if err != nil {
return to.ErrorResult(err)
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.DeleteOrgHook(org, id)
if err != nil {
return to.ErrorResult(fmt.Errorf("delete org %s hook %d err: %v", org, id, err))
}
return to.TextResult(map[string]any{"status": "deleted", "org": org, "hook_id": id})
}

View File

@@ -0,0 +1,241 @@
package organization
import (
"context"
"errors"
"fmt"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/gitea"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/log"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/params"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/to"
gitea_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
const (
ListOrgMembersToolName = "list_org_members"
CheckOrgMembershipToolName = "check_org_membership"
RemoveOrgMemberToolName = "remove_org_member"
ListOrgPublicMembersToolName = "list_org_public_members"
CheckOrgPublicMemberToolName = "check_org_public_member"
SetOrgPublicMemberToolName = "set_org_public_member"
GetOrgPermissionsToolName = "get_org_permissions"
)
var (
ListOrgMembersTool = mcp.NewTool(
ListOrgMembersToolName,
mcp.WithDescription("List an organization's members"),
mcp.WithString("org", mcp.Required(), mcp.Description("organization name")),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)),
)
CheckOrgMembershipTool = mcp.NewTool(
CheckOrgMembershipToolName,
mcp.WithDescription("Check if a user is a member of an organization"),
mcp.WithString("org", mcp.Required(), mcp.Description("organization name")),
mcp.WithString("username", mcp.Required(), mcp.Description("username to check")),
)
RemoveOrgMemberTool = mcp.NewTool(
RemoveOrgMemberToolName,
mcp.WithDescription("Remove a member from an organization"),
mcp.WithString("org", mcp.Required(), mcp.Description("organization name")),
mcp.WithString("username", mcp.Required(), mcp.Description("username to remove")),
)
ListOrgPublicMembersTool = mcp.NewTool(
ListOrgPublicMembersToolName,
mcp.WithDescription("List an organization's public members"),
mcp.WithString("org", mcp.Required(), mcp.Description("organization name")),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)),
)
CheckOrgPublicMemberTool = mcp.NewTool(
CheckOrgPublicMemberToolName,
mcp.WithDescription("Check if a user is a public member of an organization"),
mcp.WithString("org", mcp.Required(), mcp.Description("organization name")),
mcp.WithString("username", mcp.Required(), mcp.Description("username to check")),
)
SetOrgPublicMemberTool = mcp.NewTool(
SetOrgPublicMemberToolName,
mcp.WithDescription("Set or remove a user's public membership in an organization"),
mcp.WithString("org", mcp.Required(), mcp.Description("organization name")),
mcp.WithString("username", mcp.Required(), mcp.Description("username")),
mcp.WithBoolean("visible", mcp.Required(), mcp.Description("true to make public, false to make private")),
)
GetOrgPermissionsTool = mcp.NewTool(
GetOrgPermissionsToolName,
mcp.WithDescription("Get a user's permissions in an organization"),
mcp.WithString("org", mcp.Required(), mcp.Description("organization name")),
mcp.WithString("username", mcp.Required(), mcp.Description("username")),
)
)
func init() {
Tool.RegisterRead(server.ServerTool{Tool: ListOrgMembersTool, Handler: ListOrgMembersFn})
Tool.RegisterRead(server.ServerTool{Tool: CheckOrgMembershipTool, Handler: CheckOrgMembershipFn})
Tool.RegisterRead(server.ServerTool{Tool: ListOrgPublicMembersTool, Handler: ListOrgPublicMembersFn})
Tool.RegisterRead(server.ServerTool{Tool: CheckOrgPublicMemberTool, Handler: CheckOrgPublicMemberFn})
Tool.RegisterRead(server.ServerTool{Tool: GetOrgPermissionsTool, Handler: GetOrgPermissionsFn})
Tool.RegisterWrite(server.ServerTool{Tool: RemoveOrgMemberTool, Handler: RemoveOrgMemberFn})
Tool.RegisterWrite(server.ServerTool{Tool: SetOrgPublicMemberTool, Handler: SetOrgPublicMemberFn})
}
func ListOrgMembersFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListOrgMembersFn")
org, ok := req.GetArguments()["org"].(string)
if !ok {
return to.ErrorResult(errors.New("org is required"))
}
page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
members, _, err := client.ListOrgMembership(org, gitea_sdk.ListOrgMembershipOption{
ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)},
})
if err != nil {
return to.ErrorResult(fmt.Errorf("list org %s members err: %v", org, err))
}
return to.TextResult(members)
}
func CheckOrgMembershipFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CheckOrgMembershipFn")
org, ok := req.GetArguments()["org"].(string)
if !ok {
return to.ErrorResult(errors.New("org is required"))
}
username, ok := req.GetArguments()["username"].(string)
if !ok {
return to.ErrorResult(errors.New("username is required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
isMember, _, err := client.CheckOrgMembership(org, username)
if err != nil {
return to.ErrorResult(fmt.Errorf("check org %s membership for %s err: %v", org, username, err))
}
return to.TextResult(map[string]any{"org": org, "username": username, "is_member": isMember})
}
func RemoveOrgMemberFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called RemoveOrgMemberFn")
org, ok := req.GetArguments()["org"].(string)
if !ok {
return to.ErrorResult(errors.New("org is required"))
}
username, ok := req.GetArguments()["username"].(string)
if !ok {
return to.ErrorResult(errors.New("username is required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.DeleteOrgMembership(org, username)
if err != nil {
return to.ErrorResult(fmt.Errorf("remove member %s from org %s err: %v", username, org, err))
}
return to.TextResult(map[string]string{"status": "removed", "org": org, "username": username})
}
func ListOrgPublicMembersFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListOrgPublicMembersFn")
org, ok := req.GetArguments()["org"].(string)
if !ok {
return to.ErrorResult(errors.New("org is required"))
}
page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
members, _, err := client.ListPublicOrgMembership(org, gitea_sdk.ListOrgMembershipOption{
ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)},
})
if err != nil {
return to.ErrorResult(fmt.Errorf("list org %s public members err: %v", org, err))
}
return to.TextResult(members)
}
func CheckOrgPublicMemberFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CheckOrgPublicMemberFn")
org, ok := req.GetArguments()["org"].(string)
if !ok {
return to.ErrorResult(errors.New("org is required"))
}
username, ok := req.GetArguments()["username"].(string)
if !ok {
return to.ErrorResult(errors.New("username is required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
isPublic, _, err := client.CheckPublicOrgMembership(org, username)
if err != nil {
return to.ErrorResult(fmt.Errorf("check org %s public membership for %s err: %v", org, username, err))
}
return to.TextResult(map[string]any{"org": org, "username": username, "is_public_member": isPublic})
}
func SetOrgPublicMemberFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called SetOrgPublicMemberFn")
org, ok := req.GetArguments()["org"].(string)
if !ok {
return to.ErrorResult(errors.New("org is required"))
}
username, ok := req.GetArguments()["username"].(string)
if !ok {
return to.ErrorResult(errors.New("username is required"))
}
visible, ok := req.GetArguments()["visible"].(bool)
if !ok {
return to.ErrorResult(errors.New("visible is required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.SetPublicOrgMembership(org, username, visible)
if err != nil {
return to.ErrorResult(fmt.Errorf("set public membership for %s in org %s err: %v", username, org, err))
}
return to.TextResult(map[string]any{"org": org, "username": username, "visible": visible})
}
func GetOrgPermissionsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetOrgPermissionsFn")
org, ok := req.GetArguments()["org"].(string)
if !ok {
return to.ErrorResult(errors.New("org is required"))
}
username, ok := req.GetArguments()["username"].(string)
if !ok {
return to.ErrorResult(errors.New("username is required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
perms, _, err := client.GetOrgPermissions(org, username)
if err != nil {
return to.ErrorResult(fmt.Errorf("get permissions for %s in org %s err: %v", username, org, err))
}
return to.TextResult(perms)
}

View File

@@ -0,0 +1,319 @@
package organization
import (
"context"
"errors"
"fmt"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/gitea"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/log"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/params"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/to"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/tool"
gitea_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
var Tool = tool.New()
const (
ListOrgsToolName = "list_orgs"
GetOrgToolName = "get_org"
CreateOrgToolName = "create_org"
EditOrgToolName = "edit_org"
DeleteOrgToolName = "delete_org"
RenameOrgToolName = "rename_org"
ListOrgReposToolName = "list_org_repos"
CreateOrgRepoToolName = "create_org_repo"
)
var (
ListOrgsTool = mcp.NewTool(
ListOrgsToolName,
mcp.WithDescription("List all visible organizations"),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)),
)
GetOrgTool = mcp.NewTool(
GetOrgToolName,
mcp.WithDescription("Get an organization by name"),
mcp.WithString("org", mcp.Required(), mcp.Description("organization name")),
)
CreateOrgTool = mcp.NewTool(
CreateOrgToolName,
mcp.WithDescription("Create an organization"),
mcp.WithString("username", mcp.Required(), mcp.Description("organization username")),
mcp.WithString("full_name", mcp.Description("organization full name")),
mcp.WithString("description", mcp.Description("organization description")),
mcp.WithString("website", mcp.Description("organization website")),
mcp.WithString("location", mcp.Description("organization location")),
mcp.WithString("visibility", mcp.Description("visibility: public, limited, or private"), mcp.DefaultString("public")),
)
EditOrgTool = mcp.NewTool(
EditOrgToolName,
mcp.WithDescription("Edit an organization"),
mcp.WithString("org", mcp.Required(), mcp.Description("organization name")),
mcp.WithString("full_name", mcp.Description("organization full name")),
mcp.WithString("description", mcp.Description("organization description")),
mcp.WithString("website", mcp.Description("organization website")),
mcp.WithString("location", mcp.Description("organization location")),
mcp.WithString("visibility", mcp.Description("visibility: public, limited, or private")),
)
DeleteOrgTool = mcp.NewTool(
DeleteOrgToolName,
mcp.WithDescription("Delete an organization"),
mcp.WithString("org", mcp.Required(), mcp.Description("organization name")),
)
RenameOrgTool = mcp.NewTool(
RenameOrgToolName,
mcp.WithDescription("Rename an organization"),
mcp.WithString("org", mcp.Required(), mcp.Description("current organization name")),
mcp.WithString("new_name", mcp.Required(), mcp.Description("new organization name")),
)
ListOrgReposTool = mcp.NewTool(
ListOrgReposToolName,
mcp.WithDescription("List an organization's repositories"),
mcp.WithString("org", mcp.Required(), mcp.Description("organization name")),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)),
)
CreateOrgRepoTool = mcp.NewTool(
CreateOrgRepoToolName,
mcp.WithDescription("Create a repository in an organization"),
mcp.WithString("org", mcp.Required(), mcp.Description("organization name")),
mcp.WithString("name", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("description", mcp.Description("repository description")),
mcp.WithBoolean("private", mcp.Description("whether the repository is private")),
mcp.WithBoolean("auto_init", mcp.Description("whether to auto-initialize with README")),
mcp.WithString("default_branch", mcp.Description("default branch name")),
mcp.WithString("gitignores", mcp.Description("gitignore template")),
mcp.WithString("license", mcp.Description("license template")),
)
)
func init() {
Tool.RegisterRead(server.ServerTool{Tool: ListOrgsTool, Handler: ListOrgsFn})
Tool.RegisterRead(server.ServerTool{Tool: GetOrgTool, Handler: GetOrgFn})
Tool.RegisterRead(server.ServerTool{Tool: ListOrgReposTool, Handler: ListOrgReposFn})
Tool.RegisterWrite(server.ServerTool{Tool: CreateOrgTool, Handler: CreateOrgFn})
Tool.RegisterWrite(server.ServerTool{Tool: EditOrgTool, Handler: EditOrgFn})
Tool.RegisterWrite(server.ServerTool{Tool: DeleteOrgTool, Handler: DeleteOrgFn})
Tool.RegisterWrite(server.ServerTool{Tool: RenameOrgTool, Handler: RenameOrgFn})
Tool.RegisterWrite(server.ServerTool{Tool: CreateOrgRepoTool, Handler: CreateOrgRepoFn})
}
func ListOrgsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListOrgsFn")
page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
orgs, _, err := client.ListOrgs(gitea_sdk.ListOrgsOptions{
ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)},
})
if err != nil {
return to.ErrorResult(fmt.Errorf("list orgs err: %v", err))
}
return to.TextResult(orgs)
}
func GetOrgFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetOrgFn")
org, ok := req.GetArguments()["org"].(string)
if !ok {
return to.ErrorResult(errors.New("org is required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
organization, _, err := client.GetOrg(org)
if err != nil {
return to.ErrorResult(fmt.Errorf("get org %s err: %v", org, err))
}
return to.TextResult(organization)
}
func CreateOrgFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CreateOrgFn")
username, ok := req.GetArguments()["username"].(string)
if !ok {
return to.ErrorResult(errors.New("username is required"))
}
opt := gitea_sdk.CreateOrgOption{
Name: username,
}
if v, ok := req.GetArguments()["full_name"].(string); ok {
opt.FullName = v
}
if v, ok := req.GetArguments()["description"].(string); ok {
opt.Description = v
}
if v, ok := req.GetArguments()["website"].(string); ok {
opt.Website = v
}
if v, ok := req.GetArguments()["location"].(string); ok {
opt.Location = v
}
if v, ok := req.GetArguments()["visibility"].(string); ok {
opt.Visibility = gitea_sdk.VisibleType(v)
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
organization, _, err := client.CreateOrg(opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("create org %s err: %v", username, err))
}
return to.TextResult(organization)
}
func EditOrgFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called EditOrgFn")
org, ok := req.GetArguments()["org"].(string)
if !ok {
return to.ErrorResult(errors.New("org is required"))
}
opt := gitea_sdk.EditOrgOption{}
if v, ok := req.GetArguments()["full_name"].(string); ok {
opt.FullName = v
}
if v, ok := req.GetArguments()["description"].(string); ok {
opt.Description = v
}
if v, ok := req.GetArguments()["website"].(string); ok {
opt.Website = v
}
if v, ok := req.GetArguments()["location"].(string); ok {
opt.Location = v
}
if v, ok := req.GetArguments()["visibility"].(string); ok {
opt.Visibility = gitea_sdk.VisibleType(v)
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.EditOrg(org, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("edit org %s err: %v", org, err))
}
updated, _, err := client.GetOrg(org)
if err != nil {
return to.ErrorResult(fmt.Errorf("get org %s after edit err: %v", org, err))
}
return to.TextResult(updated)
}
func DeleteOrgFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called DeleteOrgFn")
org, ok := req.GetArguments()["org"].(string)
if !ok {
return to.ErrorResult(errors.New("org is required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.DeleteOrg(org)
if err != nil {
return to.ErrorResult(fmt.Errorf("delete org %s err: %v", org, err))
}
return to.TextResult(map[string]string{"status": "deleted", "org": org})
}
func RenameOrgFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called RenameOrgFn")
org, ok := req.GetArguments()["org"].(string)
if !ok {
return to.ErrorResult(errors.New("org is required"))
}
newName, ok := req.GetArguments()["new_name"].(string)
if !ok {
return to.ErrorResult(errors.New("new_name is required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.RenameOrg(org, gitea_sdk.RenameOrgOption{NewName: newName})
if err != nil {
return to.ErrorResult(fmt.Errorf("rename org %s to %s err: %v", org, newName, err))
}
return to.TextResult(map[string]string{"status": "renamed", "old_name": org, "new_name": newName})
}
func ListOrgReposFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListOrgReposFn")
org, ok := req.GetArguments()["org"].(string)
if !ok {
return to.ErrorResult(errors.New("org is required"))
}
page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
repos, _, err := client.ListOrgRepos(org, gitea_sdk.ListOrgReposOptions{
ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)},
})
if err != nil {
return to.ErrorResult(fmt.Errorf("list org %s repos err: %v", org, err))
}
return to.TextResult(repos)
}
func CreateOrgRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CreateOrgRepoFn")
org, ok := req.GetArguments()["org"].(string)
if !ok {
return to.ErrorResult(errors.New("org is required"))
}
name, ok := req.GetArguments()["name"].(string)
if !ok {
return to.ErrorResult(errors.New("name is required"))
}
opt := gitea_sdk.CreateRepoOption{
Name: name,
}
if v, ok := req.GetArguments()["description"].(string); ok {
opt.Description = v
}
if v, ok := req.GetArguments()["private"].(bool); ok {
opt.Private = v
}
if v, ok := req.GetArguments()["auto_init"].(bool); ok {
opt.AutoInit = v
}
if v, ok := req.GetArguments()["default_branch"].(string); ok {
opt.DefaultBranch = v
}
if v, ok := req.GetArguments()["gitignores"].(string); ok {
opt.Gitignores = v
}
if v, ok := req.GetArguments()["license"].(string); ok {
opt.License = v
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
repo, _, err := client.CreateOrgRepo(org, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("create repo %s/%s err: %v", org, name, err))
}
return to.TextResult(repo)
}

View File

@@ -0,0 +1,427 @@
package organization
import (
"context"
"errors"
"fmt"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/gitea"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/log"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/params"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/to"
gitea_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
const (
ListOrgTeamsToolName = "list_org_teams"
GetTeamToolName = "get_team"
CreateTeamToolName = "create_team"
EditTeamToolName = "edit_team"
DeleteTeamToolName = "delete_team"
ListTeamMembersToolName = "list_team_members"
GetTeamMemberToolName = "get_team_member"
AddTeamMemberToolName = "add_team_member"
RemoveTeamMemberToolName = "remove_team_member"
ListTeamReposToolName = "list_team_repos"
AddTeamRepoToolName = "add_team_repo"
RemoveTeamRepoToolName = "remove_team_repo"
)
var (
ListOrgTeamsTool = mcp.NewTool(
ListOrgTeamsToolName,
mcp.WithDescription("List an organization's teams"),
mcp.WithString("org", mcp.Required(), mcp.Description("organization name")),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)),
)
GetTeamTool = mcp.NewTool(
GetTeamToolName,
mcp.WithDescription("Get a team by ID"),
mcp.WithNumber("id", mcp.Required(), mcp.Description("team ID")),
)
CreateTeamTool = mcp.NewTool(
CreateTeamToolName,
mcp.WithDescription("Create a team in an organization"),
mcp.WithString("org", mcp.Required(), mcp.Description("organization name")),
mcp.WithString("name", mcp.Required(), mcp.Description("team name")),
mcp.WithString("description", mcp.Description("team description")),
mcp.WithString("permission", mcp.Description("team permission: read, write, admin"), mcp.DefaultString("read")),
mcp.WithBoolean("can_create_org_repo", mcp.Description("whether team members can create repos in the org")),
mcp.WithBoolean("includes_all_repositories", mcp.Description("whether team has access to all repos")),
)
EditTeamTool = mcp.NewTool(
EditTeamToolName,
mcp.WithDescription("Edit a team"),
mcp.WithNumber("id", mcp.Required(), mcp.Description("team ID")),
mcp.WithString("name", mcp.Description("team name")),
mcp.WithString("description", mcp.Description("team description")),
mcp.WithString("permission", mcp.Description("team permission: read, write, admin")),
mcp.WithBoolean("can_create_org_repo", mcp.Description("whether team members can create repos in the org")),
mcp.WithBoolean("includes_all_repositories", mcp.Description("whether team has access to all repos")),
)
DeleteTeamTool = mcp.NewTool(
DeleteTeamToolName,
mcp.WithDescription("Delete a team"),
mcp.WithNumber("id", mcp.Required(), mcp.Description("team ID")),
)
ListTeamMembersTool = mcp.NewTool(
ListTeamMembersToolName,
mcp.WithDescription("List a team's members"),
mcp.WithNumber("id", mcp.Required(), mcp.Description("team ID")),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)),
)
GetTeamMemberTool = mcp.NewTool(
GetTeamMemberToolName,
mcp.WithDescription("Get a specific member of a team"),
mcp.WithNumber("id", mcp.Required(), mcp.Description("team ID")),
mcp.WithString("username", mcp.Required(), mcp.Description("username")),
)
AddTeamMemberTool = mcp.NewTool(
AddTeamMemberToolName,
mcp.WithDescription("Add a member to a team"),
mcp.WithNumber("id", mcp.Required(), mcp.Description("team ID")),
mcp.WithString("username", mcp.Required(), mcp.Description("username to add")),
)
RemoveTeamMemberTool = mcp.NewTool(
RemoveTeamMemberToolName,
mcp.WithDescription("Remove a member from a team"),
mcp.WithNumber("id", mcp.Required(), mcp.Description("team ID")),
mcp.WithString("username", mcp.Required(), mcp.Description("username to remove")),
)
ListTeamReposTool = mcp.NewTool(
ListTeamReposToolName,
mcp.WithDescription("List a team's repositories"),
mcp.WithNumber("id", mcp.Required(), mcp.Description("team ID")),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)),
)
AddTeamRepoTool = mcp.NewTool(
AddTeamRepoToolName,
mcp.WithDescription("Add a repository to a team"),
mcp.WithNumber("id", mcp.Required(), mcp.Description("team ID")),
mcp.WithString("org", mcp.Required(), mcp.Description("organization name")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
)
RemoveTeamRepoTool = mcp.NewTool(
RemoveTeamRepoToolName,
mcp.WithDescription("Remove a repository from a team"),
mcp.WithNumber("id", mcp.Required(), mcp.Description("team ID")),
mcp.WithString("org", mcp.Required(), mcp.Description("organization name")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
)
)
func init() {
Tool.RegisterRead(server.ServerTool{Tool: ListOrgTeamsTool, Handler: ListOrgTeamsFn})
Tool.RegisterRead(server.ServerTool{Tool: GetTeamTool, Handler: GetTeamFn})
Tool.RegisterRead(server.ServerTool{Tool: ListTeamMembersTool, Handler: ListTeamMembersFn})
Tool.RegisterRead(server.ServerTool{Tool: GetTeamMemberTool, Handler: GetTeamMemberFn})
Tool.RegisterRead(server.ServerTool{Tool: ListTeamReposTool, Handler: ListTeamReposFn})
Tool.RegisterWrite(server.ServerTool{Tool: CreateTeamTool, Handler: CreateTeamFn})
Tool.RegisterWrite(server.ServerTool{Tool: EditTeamTool, Handler: EditTeamFn})
Tool.RegisterWrite(server.ServerTool{Tool: DeleteTeamTool, Handler: DeleteTeamFn})
Tool.RegisterWrite(server.ServerTool{Tool: AddTeamMemberTool, Handler: AddTeamMemberFn})
Tool.RegisterWrite(server.ServerTool{Tool: RemoveTeamMemberTool, Handler: RemoveTeamMemberFn})
Tool.RegisterWrite(server.ServerTool{Tool: AddTeamRepoTool, Handler: AddTeamRepoFn})
Tool.RegisterWrite(server.ServerTool{Tool: RemoveTeamRepoTool, Handler: RemoveTeamRepoFn})
}
func ListOrgTeamsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListOrgTeamsFn")
org, ok := req.GetArguments()["org"].(string)
if !ok {
return to.ErrorResult(errors.New("org is required"))
}
page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
teams, _, err := client.ListOrgTeams(org, gitea_sdk.ListTeamsOptions{
ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)},
})
if err != nil {
return to.ErrorResult(fmt.Errorf("list org %s teams err: %v", org, err))
}
return to.TextResult(teams)
}
func GetTeamFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetTeamFn")
id, err := params.GetIndex(req.GetArguments(), "id")
if err != nil {
return to.ErrorResult(err)
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
team, _, err := client.GetTeam(id)
if err != nil {
return to.ErrorResult(fmt.Errorf("get team %d err: %v", id, err))
}
return to.TextResult(team)
}
func CreateTeamFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CreateTeamFn")
org, ok := req.GetArguments()["org"].(string)
if !ok {
return to.ErrorResult(errors.New("org is required"))
}
name, ok := req.GetArguments()["name"].(string)
if !ok {
return to.ErrorResult(errors.New("name is required"))
}
opt := gitea_sdk.CreateTeamOption{
Name: name,
}
if v, ok := req.GetArguments()["description"].(string); ok {
opt.Description = v
}
if v, ok := req.GetArguments()["permission"].(string); ok {
opt.Permission = gitea_sdk.AccessMode(v)
}
if v, ok := req.GetArguments()["can_create_org_repo"].(bool); ok {
opt.CanCreateOrgRepo = v
}
if v, ok := req.GetArguments()["includes_all_repositories"].(bool); ok {
opt.IncludesAllRepositories = v
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
team, _, err := client.CreateTeam(org, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("create team %s in org %s err: %v", name, org, err))
}
return to.TextResult(team)
}
func EditTeamFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called EditTeamFn")
id, err := params.GetIndex(req.GetArguments(), "id")
if err != nil {
return to.ErrorResult(err)
}
opt := gitea_sdk.EditTeamOption{}
if v, ok := req.GetArguments()["name"].(string); ok {
opt.Name = v
}
if v, ok := req.GetArguments()["description"].(string); ok {
opt.Description = &v
}
if v, ok := req.GetArguments()["permission"].(string); ok {
perm := gitea_sdk.AccessMode(v)
opt.Permission = perm
}
if v, ok := req.GetArguments()["can_create_org_repo"].(bool); ok {
opt.CanCreateOrgRepo = &v
}
if v, ok := req.GetArguments()["includes_all_repositories"].(bool); ok {
opt.IncludesAllRepositories = &v
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.EditTeam(id, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("edit team %d err: %v", id, err))
}
team, _, err := client.GetTeam(id)
if err != nil {
return to.ErrorResult(fmt.Errorf("get team %d after edit err: %v", id, err))
}
return to.TextResult(team)
}
func DeleteTeamFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called DeleteTeamFn")
id, err := params.GetIndex(req.GetArguments(), "id")
if err != nil {
return to.ErrorResult(err)
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.DeleteTeam(id)
if err != nil {
return to.ErrorResult(fmt.Errorf("delete team %d err: %v", id, err))
}
return to.TextResult(map[string]any{"status": "deleted", "team_id": id})
}
func ListTeamMembersFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListTeamMembersFn")
id, err := params.GetIndex(req.GetArguments(), "id")
if err != nil {
return to.ErrorResult(err)
}
page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
members, _, err := client.ListTeamMembers(id, gitea_sdk.ListTeamMembersOptions{
ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)},
})
if err != nil {
return to.ErrorResult(fmt.Errorf("list team %d members err: %v", id, err))
}
return to.TextResult(members)
}
func GetTeamMemberFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetTeamMemberFn")
id, err := params.GetIndex(req.GetArguments(), "id")
if err != nil {
return to.ErrorResult(err)
}
username, ok := req.GetArguments()["username"].(string)
if !ok {
return to.ErrorResult(errors.New("username is required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
member, _, err := client.GetTeamMember(id, username)
if err != nil {
return to.ErrorResult(fmt.Errorf("get team %d member %s err: %v", id, username, err))
}
return to.TextResult(member)
}
func AddTeamMemberFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called AddTeamMemberFn")
id, err := params.GetIndex(req.GetArguments(), "id")
if err != nil {
return to.ErrorResult(err)
}
username, ok := req.GetArguments()["username"].(string)
if !ok {
return to.ErrorResult(errors.New("username is required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.AddTeamMember(id, username)
if err != nil {
return to.ErrorResult(fmt.Errorf("add member %s to team %d err: %v", username, id, err))
}
return to.TextResult(map[string]any{"status": "added", "team_id": id, "username": username})
}
func RemoveTeamMemberFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called RemoveTeamMemberFn")
id, err := params.GetIndex(req.GetArguments(), "id")
if err != nil {
return to.ErrorResult(err)
}
username, ok := req.GetArguments()["username"].(string)
if !ok {
return to.ErrorResult(errors.New("username is required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.RemoveTeamMember(id, username)
if err != nil {
return to.ErrorResult(fmt.Errorf("remove member %s from team %d err: %v", username, id, err))
}
return to.TextResult(map[string]any{"status": "removed", "team_id": id, "username": username})
}
func ListTeamReposFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListTeamReposFn")
id, err := params.GetIndex(req.GetArguments(), "id")
if err != nil {
return to.ErrorResult(err)
}
page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
repos, _, err := client.ListTeamRepositories(id, gitea_sdk.ListTeamRepositoriesOptions{
ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)},
})
if err != nil {
return to.ErrorResult(fmt.Errorf("list team %d repos err: %v", id, err))
}
return to.TextResult(repos)
}
func AddTeamRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called AddTeamRepoFn")
id, err := params.GetIndex(req.GetArguments(), "id")
if err != nil {
return to.ErrorResult(err)
}
org, ok := req.GetArguments()["org"].(string)
if !ok {
return to.ErrorResult(errors.New("org is required"))
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(errors.New("repo is required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.AddTeamRepository(id, org, repo)
if err != nil {
return to.ErrorResult(fmt.Errorf("add repo %s/%s to team %d err: %v", org, repo, id, err))
}
return to.TextResult(map[string]any{"status": "added", "team_id": id, "org": org, "repo": repo})
}
func RemoveTeamRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called RemoveTeamRepoFn")
id, err := params.GetIndex(req.GetArguments(), "id")
if err != nil {
return to.ErrorResult(err)
}
org, ok := req.GetArguments()["org"].(string)
if !ok {
return to.ErrorResult(errors.New("org is required"))
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(errors.New("repo is required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.RemoveTeamRepository(id, org, repo)
if err != nil {
return to.ErrorResult(fmt.Errorf("remove repo %s/%s from team %d err: %v", org, repo, id, err))
}
return to.TextResult(map[string]any{"status": "removed", "team_id": id, "org": org, "repo": repo})
}

View File

@@ -0,0 +1,242 @@
package packages
import (
"context"
"errors"
"fmt"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/gitea"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/log"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/params"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/to"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/tool"
gitea_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
var Tool = tool.New()
const (
ListPackagesToolName = "list_packages"
GetPackageToolName = "get_package"
DeletePackageToolName = "delete_package"
ListPackageFilesToolName = "list_package_files"
GetLatestPackageToolName = "get_latest_package"
LinkPackageToolName = "link_package"
UnlinkPackageToolName = "unlink_package"
)
var (
ListPackagesTool = mcp.NewTool(
ListPackagesToolName,
mcp.WithDescription("List packages for a user or organization"),
mcp.WithString("owner", mcp.Required(), mcp.Description("package owner (user or org)")),
mcp.WithString("type", mcp.Description("package type filter (e.g., generic, container, npm, pypi, rubygems, maven, nuget, debian, alpine, go, cargo, chef, composer, conan, conda, cran, pub, helm, rpm, swift, vagrant)")),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)),
)
GetPackageTool = mcp.NewTool(
GetPackageToolName,
mcp.WithDescription("Get a specific package by type, name, and version"),
mcp.WithString("owner", mcp.Required(), mcp.Description("package owner")),
mcp.WithString("type", mcp.Required(), mcp.Description("package type (e.g., generic, container, npm)")),
mcp.WithString("name", mcp.Required(), mcp.Description("package name")),
mcp.WithString("version", mcp.Required(), mcp.Description("package version")),
)
DeletePackageTool = mcp.NewTool(
DeletePackageToolName,
mcp.WithDescription("Delete a specific package version"),
mcp.WithString("owner", mcp.Required(), mcp.Description("package owner")),
mcp.WithString("type", mcp.Required(), mcp.Description("package type")),
mcp.WithString("name", mcp.Required(), mcp.Description("package name")),
mcp.WithString("version", mcp.Required(), mcp.Description("package version")),
)
ListPackageFilesTool = mcp.NewTool(
ListPackageFilesToolName,
mcp.WithDescription("List files of a package version"),
mcp.WithString("owner", mcp.Required(), mcp.Description("package owner")),
mcp.WithString("type", mcp.Required(), mcp.Description("package type")),
mcp.WithString("name", mcp.Required(), mcp.Description("package name")),
mcp.WithString("version", mcp.Required(), mcp.Description("package version")),
)
GetLatestPackageTool = mcp.NewTool(
GetLatestPackageToolName,
mcp.WithDescription("Get the latest version of a package"),
mcp.WithString("owner", mcp.Required(), mcp.Description("package owner")),
mcp.WithString("type", mcp.Required(), mcp.Description("package type")),
mcp.WithString("name", mcp.Required(), mcp.Description("package name")),
)
LinkPackageTool = mcp.NewTool(
LinkPackageToolName,
mcp.WithDescription("Link a package to a repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("package owner")),
mcp.WithString("type", mcp.Required(), mcp.Description("package type")),
mcp.WithString("name", mcp.Required(), mcp.Description("package name")),
mcp.WithString("repo_name", mcp.Required(), mcp.Description("repository name to link to")),
)
UnlinkPackageTool = mcp.NewTool(
UnlinkPackageToolName,
mcp.WithDescription("Unlink a package from its repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("package owner")),
mcp.WithString("type", mcp.Required(), mcp.Description("package type")),
mcp.WithString("name", mcp.Required(), mcp.Description("package name")),
)
)
func init() {
Tool.RegisterRead(server.ServerTool{Tool: ListPackagesTool, Handler: ListPackagesFn})
Tool.RegisterRead(server.ServerTool{Tool: GetPackageTool, Handler: GetPackageFn})
Tool.RegisterRead(server.ServerTool{Tool: ListPackageFilesTool, Handler: ListPackageFilesFn})
Tool.RegisterRead(server.ServerTool{Tool: GetLatestPackageTool, Handler: GetLatestPackageFn})
Tool.RegisterWrite(server.ServerTool{Tool: DeletePackageTool, Handler: DeletePackageFn})
Tool.RegisterWrite(server.ServerTool{Tool: LinkPackageTool, Handler: LinkPackageFn})
Tool.RegisterWrite(server.ServerTool{Tool: UnlinkPackageTool, Handler: UnlinkPackageFn})
}
func ListPackagesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListPackagesFn")
owner, _ := req.GetArguments()["owner"].(string)
if owner == "" {
return to.ErrorResult(errors.New("owner is required"))
}
page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
opt := gitea_sdk.ListPackagesOptions{
ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)},
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
packages, _, err := client.ListPackages(owner, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("list packages err: %v", err))
}
return to.TextResult(packages)
}
func GetPackageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetPackageFn")
owner, _ := req.GetArguments()["owner"].(string)
pkgType, _ := req.GetArguments()["type"].(string)
name, _ := req.GetArguments()["name"].(string)
version, _ := req.GetArguments()["version"].(string)
if owner == "" || pkgType == "" || name == "" || version == "" {
return to.ErrorResult(errors.New("owner, type, name, and version are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
pkg, _, err := client.GetPackage(owner, pkgType, name, version)
if err != nil {
return to.ErrorResult(fmt.Errorf("get package err: %v", err))
}
return to.TextResult(pkg)
}
func DeletePackageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called DeletePackageFn")
owner, _ := req.GetArguments()["owner"].(string)
pkgType, _ := req.GetArguments()["type"].(string)
name, _ := req.GetArguments()["name"].(string)
version, _ := req.GetArguments()["version"].(string)
if owner == "" || pkgType == "" || name == "" || version == "" {
return to.ErrorResult(errors.New("owner, type, name, and version are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.DeletePackage(owner, pkgType, name, version)
if err != nil {
return to.ErrorResult(fmt.Errorf("delete package err: %v", err))
}
return to.TextResult(map[string]string{"status": "deleted"})
}
func ListPackageFilesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListPackageFilesFn")
owner, _ := req.GetArguments()["owner"].(string)
pkgType, _ := req.GetArguments()["type"].(string)
name, _ := req.GetArguments()["name"].(string)
version, _ := req.GetArguments()["version"].(string)
if owner == "" || pkgType == "" || name == "" || version == "" {
return to.ErrorResult(errors.New("owner, type, name, and version are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
files, _, err := client.ListPackageFiles(owner, pkgType, name, version)
if err != nil {
return to.ErrorResult(fmt.Errorf("list package files err: %v", err))
}
return to.TextResult(files)
}
func GetLatestPackageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetLatestPackageFn")
owner, _ := req.GetArguments()["owner"].(string)
pkgType, _ := req.GetArguments()["type"].(string)
name, _ := req.GetArguments()["name"].(string)
if owner == "" || pkgType == "" || name == "" {
return to.ErrorResult(errors.New("owner, type, and name are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
pkg, _, err := client.GetLatestPackage(owner, pkgType, name)
if err != nil {
return to.ErrorResult(fmt.Errorf("get latest package err: %v", err))
}
return to.TextResult(pkg)
}
func LinkPackageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called LinkPackageFn")
owner, _ := req.GetArguments()["owner"].(string)
pkgType, _ := req.GetArguments()["type"].(string)
name, _ := req.GetArguments()["name"].(string)
repoName, _ := req.GetArguments()["repo_name"].(string)
if owner == "" || pkgType == "" || name == "" || repoName == "" {
return to.ErrorResult(errors.New("owner, type, name, and repo_name are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.LinkPackage(owner, pkgType, name, repoName)
if err != nil {
return to.ErrorResult(fmt.Errorf("link package err: %v", err))
}
return to.TextResult(map[string]string{"status": "linked", "repo": repoName})
}
func UnlinkPackageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called UnlinkPackageFn")
owner, _ := req.GetArguments()["owner"].(string)
pkgType, _ := req.GetArguments()["type"].(string)
name, _ := req.GetArguments()["name"].(string)
if owner == "" || pkgType == "" || name == "" {
return to.ErrorResult(errors.New("owner, type, and name are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.UnlinkPackage(owner, pkgType, name)
if err != nil {
return to.ErrorResult(fmt.Errorf("unlink package err: %v", err))
}
return to.TextResult(map[string]string{"status": "unlinked"})
}

View File

@@ -2,12 +2,14 @@ package pull
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"gitea.com/gitea/gitea-mcp/pkg/gitea" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/gitea"
"gitea.com/gitea/gitea-mcp/pkg/log" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/log"
"gitea.com/gitea/gitea-mcp/pkg/to" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/params"
"gitea.com/gitea/gitea-mcp/pkg/tool" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/to"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/tool"
gitea_sdk "code.gitea.io/sdk/gitea" gitea_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/mcp"
@@ -18,6 +20,7 @@ var Tool = tool.New()
const ( const (
GetPullRequestByIndexToolName = "get_pull_request_by_index" GetPullRequestByIndexToolName = "get_pull_request_by_index"
GetPullRequestDiffToolName = "get_pull_request_diff"
ListRepoPullRequestsToolName = "list_repo_pull_requests" ListRepoPullRequestsToolName = "list_repo_pull_requests"
CreatePullRequestToolName = "create_pull_request" CreatePullRequestToolName = "create_pull_request"
CreatePullRequestReviewerToolName = "create_pull_request_reviewer" CreatePullRequestReviewerToolName = "create_pull_request_reviewer"
@@ -29,6 +32,8 @@ const (
SubmitPullRequestReviewToolName = "submit_pull_request_review" SubmitPullRequestReviewToolName = "submit_pull_request_review"
DeletePullRequestReviewToolName = "delete_pull_request_review" DeletePullRequestReviewToolName = "delete_pull_request_review"
DismissPullRequestReviewToolName = "dismiss_pull_request_review" DismissPullRequestReviewToolName = "dismiss_pull_request_review"
MergePullRequestToolName = "merge_pull_request"
EditPullRequestToolName = "edit_pull_request"
) )
var ( var (
@@ -40,6 +45,15 @@ var (
mcp.WithNumber("index", mcp.Required(), mcp.Description("repository pull request index")), mcp.WithNumber("index", mcp.Required(), mcp.Description("repository pull request index")),
) )
GetPullRequestDiffTool = mcp.NewTool(
GetPullRequestDiffToolName,
mcp.WithDescription("get pull request diff"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("index", mcp.Required(), mcp.Description("repository pull request index")),
mcp.WithBoolean("binary", mcp.Description("whether to include binary file changes")),
)
ListRepoPullRequestsTool = mcp.NewTool( ListRepoPullRequestsTool = mcp.NewTool(
ListRepoPullRequestsToolName, ListRepoPullRequestsToolName,
mcp.WithDescription("List repository pull requests"), mcp.WithDescription("List repository pull requests"),
@@ -69,8 +83,8 @@ var (
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("index", mcp.Required(), mcp.Description("pull request index")), mcp.WithNumber("index", mcp.Required(), mcp.Description("pull request index")),
mcp.WithArray("reviewers", mcp.Description("list of reviewer usernames"), mcp.Items(map[string]interface{}{"type": "string"})), mcp.WithArray("reviewers", mcp.Description("list of reviewer usernames"), mcp.Items(map[string]any{"type": "string"})),
mcp.WithArray("team_reviewers", mcp.Description("list of team reviewer names"), mcp.Items(map[string]interface{}{"type": "string"})), mcp.WithArray("team_reviewers", mcp.Description("list of team reviewer names"), mcp.Items(map[string]any{"type": "string"})),
) )
DeletePullRequestReviewerTool = mcp.NewTool( DeletePullRequestReviewerTool = mcp.NewTool(
@@ -79,8 +93,8 @@ var (
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("index", mcp.Required(), mcp.Description("pull request index")), mcp.WithNumber("index", mcp.Required(), mcp.Description("pull request index")),
mcp.WithArray("reviewers", mcp.Description("list of reviewer usernames to remove"), mcp.Items(map[string]interface{}{"type": "string"})), mcp.WithArray("reviewers", mcp.Description("list of reviewer usernames to remove"), mcp.Items(map[string]any{"type": "string"})),
mcp.WithArray("team_reviewers", mcp.Description("list of team reviewer names to remove"), mcp.Items(map[string]interface{}{"type": "string"})), mcp.WithArray("team_reviewers", mcp.Description("list of team reviewer names to remove"), mcp.Items(map[string]any{"type": "string"})),
) )
ListPullRequestReviewsTool = mcp.NewTool( ListPullRequestReviewsTool = mcp.NewTool(
@@ -120,13 +134,13 @@ var (
mcp.WithString("state", mcp.Description("review state"), mcp.Enum("APPROVED", "REQUEST_CHANGES", "COMMENT", "PENDING")), mcp.WithString("state", mcp.Description("review state"), mcp.Enum("APPROVED", "REQUEST_CHANGES", "COMMENT", "PENDING")),
mcp.WithString("body", mcp.Description("review body/comment")), mcp.WithString("body", mcp.Description("review body/comment")),
mcp.WithString("commit_id", mcp.Description("commit SHA to review")), mcp.WithString("commit_id", mcp.Description("commit SHA to review")),
mcp.WithArray("comments", mcp.Description("inline review comments (objects with path, body, old_line_num, new_line_num)"), mcp.Items(map[string]interface{}{ mcp.WithArray("comments", mcp.Description("inline review comments (objects with path, body, old_line_num, new_line_num)"), mcp.Items(map[string]any{
"type": "object", "type": "object",
"properties": map[string]interface{}{ "properties": map[string]any{
"path": map[string]interface{}{"type": "string", "description": "file path to comment on"}, "path": map[string]any{"type": "string", "description": "file path to comment on"},
"body": map[string]interface{}{"type": "string", "description": "comment body"}, "body": map[string]any{"type": "string", "description": "comment body"},
"old_line_num": map[string]interface{}{"type": "number", "description": "line number in the old file (for deletions/changes)"}, "old_line_num": map[string]any{"type": "number", "description": "line number in the old file (for deletions/changes)"},
"new_line_num": map[string]interface{}{"type": "number", "description": "line number in the new file (for additions/changes)"}, "new_line_num": map[string]any{"type": "number", "description": "line number in the new file (for additions/changes)"},
}, },
})), })),
) )
@@ -160,6 +174,34 @@ var (
mcp.WithNumber("review_id", mcp.Required(), mcp.Description("review ID")), mcp.WithNumber("review_id", mcp.Required(), mcp.Description("review ID")),
mcp.WithString("message", mcp.Description("dismissal reason")), mcp.WithString("message", mcp.Description("dismissal reason")),
) )
MergePullRequestTool = mcp.NewTool(
MergePullRequestToolName,
mcp.WithDescription("merge a pull request"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("index", mcp.Required(), mcp.Description("pull request index")),
mcp.WithString("merge_style", mcp.Description("merge style: merge, rebase, rebase-merge, squash, fast-forward-only"), mcp.Enum("merge", "rebase", "rebase-merge", "squash", "fast-forward-only"), mcp.DefaultString("merge")),
mcp.WithString("title", mcp.Description("custom merge commit title")),
mcp.WithString("message", mcp.Description("custom merge commit message")),
mcp.WithBoolean("delete_branch", mcp.Description("delete the branch after merge"), mcp.DefaultBool(false)),
)
EditPullRequestTool = mcp.NewTool(
EditPullRequestToolName,
mcp.WithDescription("edit a pull request"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("index", mcp.Required(), mcp.Description("pull request index")),
mcp.WithString("title", mcp.Description("pull request title")),
mcp.WithString("body", mcp.Description("pull request body content")),
mcp.WithString("base", mcp.Description("pull request base branch")),
mcp.WithString("assignee", mcp.Description("username to assign")),
mcp.WithArray("assignees", mcp.Description("usernames to assign"), mcp.Items(map[string]any{"type": "string"})),
mcp.WithNumber("milestone", mcp.Description("milestone number")),
mcp.WithString("state", mcp.Description("pull request state"), mcp.Enum("open", "closed")),
mcp.WithBoolean("allow_maintainer_edit", mcp.Description("allow maintainer to edit the pull request")),
)
) )
func init() { func init() {
@@ -167,6 +209,10 @@ func init() {
Tool: GetPullRequestByIndexTool, Tool: GetPullRequestByIndexTool,
Handler: GetPullRequestByIndexFn, Handler: GetPullRequestByIndexFn,
}) })
Tool.RegisterRead(server.ServerTool{
Tool: GetPullRequestDiffTool,
Handler: GetPullRequestDiffFn,
})
Tool.RegisterRead(server.ServerTool{ Tool.RegisterRead(server.ServerTool{
Tool: ListRepoPullRequestsTool, Tool: ListRepoPullRequestsTool,
Handler: ListRepoPullRequestsFn, Handler: ListRepoPullRequestsFn,
@@ -211,62 +257,101 @@ func init() {
Tool: DismissPullRequestReviewTool, Tool: DismissPullRequestReviewTool,
Handler: DismissPullRequestReviewFn, Handler: DismissPullRequestReviewFn,
}) })
Tool.RegisterWrite(server.ServerTool{
Tool: MergePullRequestTool,
Handler: MergePullRequestFn,
})
Tool.RegisterWrite(server.ServerTool{
Tool: EditPullRequestTool,
Handler: EditPullRequestFn,
})
} }
func GetPullRequestByIndexFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func GetPullRequestByIndexFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetPullRequestByIndexFn") log.Debugf("Called GetPullRequestByIndexFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
index, ok := req.GetArguments()["index"].(float64) index, err := params.GetIndex(req.GetArguments(), "index")
if !ok { if err != nil {
return to.ErrorResult(fmt.Errorf("index is required")) return to.ErrorResult(err)
} }
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
} }
pr, _, err := client.GetPullRequest(owner, repo, int64(index)) pr, _, err := client.GetPullRequest(owner, repo, index)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("get %v/%v/pr/%v err: %v", owner, repo, int64(index), err)) return to.ErrorResult(fmt.Errorf("get %v/%v/pr/%v err: %v", owner, repo, index, err))
} }
return to.TextResult(pr) return to.TextResult(pr)
} }
func GetPullRequestDiffFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetPullRequestDiffFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(errors.New("owner is required"))
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(errors.New("repo is required"))
}
index, err := params.GetIndex(req.GetArguments(), "index")
if err != nil {
return to.ErrorResult(err)
}
binary, _ := req.GetArguments()["binary"].(bool)
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
diffBytes, _, err := client.GetPullRequestDiff(owner, repo, index, gitea_sdk.PullRequestDiffOptions{
Binary: binary,
})
if err != nil {
return to.ErrorResult(fmt.Errorf("get %v/%v/pr/%v diff err: %v", owner, repo, index, err))
}
result := map[string]any{
"diff": string(diffBytes),
"binary": binary,
"index": index,
"repo": repo,
"owner": owner,
}
return to.TextResult(result)
}
func ListRepoPullRequestsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func ListRepoPullRequestsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListRepoPullRequests") log.Debugf("Called ListRepoPullRequests")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
state, _ := req.GetArguments()["state"].(string) state, _ := req.GetArguments()["state"].(string)
sort, ok := req.GetArguments()["sort"].(string) sort, ok := req.GetArguments()["sort"].(string)
if !ok { if !ok {
sort = "recentupdate" sort = "recentupdate"
} }
milestone, _ := req.GetArguments()["milestone"].(float64) milestone := params.GetOptionalInt(req.GetArguments(), "milestone", 0)
page, ok := req.GetArguments()["page"].(float64) page := params.GetOptionalInt(req.GetArguments(), "page", 1)
if !ok { pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
page = 1
}
pageSize, ok := req.GetArguments()["pageSize"].(float64)
if !ok {
pageSize = 100
}
opt := gitea_sdk.ListPullRequestsOptions{ opt := gitea_sdk.ListPullRequestsOptions{
State: gitea_sdk.StateType(state), State: gitea_sdk.StateType(state),
Sort: sort, Sort: sort,
Milestone: int64(milestone), Milestone: milestone,
ListOptions: gitea_sdk.ListOptions{ ListOptions: gitea_sdk.ListOptions{
Page: int(page), Page: int(page),
PageSize: int(pageSize), PageSize: int(pageSize),
@@ -288,27 +373,27 @@ func CreatePullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Cal
log.Debugf("Called CreatePullRequestFn") log.Debugf("Called CreatePullRequestFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
title, ok := req.GetArguments()["title"].(string) title, ok := req.GetArguments()["title"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("title is required")) return to.ErrorResult(errors.New("title is required"))
} }
body, ok := req.GetArguments()["body"].(string) body, ok := req.GetArguments()["body"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("body is required")) return to.ErrorResult(errors.New("body is required"))
} }
head, ok := req.GetArguments()["head"].(string) head, ok := req.GetArguments()["head"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("head is required")) return to.ErrorResult(errors.New("head is required"))
} }
base, ok := req.GetArguments()["base"].(string) base, ok := req.GetArguments()["base"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("base is required")) return to.ErrorResult(errors.New("base is required"))
} }
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
if err != nil { if err != nil {
@@ -331,20 +416,20 @@ func CreatePullRequestReviewerFn(ctx context.Context, req mcp.CallToolRequest) (
log.Debugf("Called CreatePullRequestReviewerFn") log.Debugf("Called CreatePullRequestReviewerFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
index, ok := req.GetArguments()["index"].(float64) index, err := params.GetIndex(req.GetArguments(), "index")
if !ok { if err != nil {
return to.ErrorResult(fmt.Errorf("index is required")) return to.ErrorResult(err)
} }
var reviewers []string var reviewers []string
if reviewersArg, exists := req.GetArguments()["reviewers"]; exists { if reviewersArg, exists := req.GetArguments()["reviewers"]; exists {
if reviewersSlice, ok := reviewersArg.([]interface{}); ok { if reviewersSlice, ok := reviewersArg.([]any); ok {
for _, reviewer := range reviewersSlice { for _, reviewer := range reviewersSlice {
if reviewerStr, ok := reviewer.(string); ok { if reviewerStr, ok := reviewer.(string); ok {
reviewers = append(reviewers, reviewerStr) reviewers = append(reviewers, reviewerStr)
@@ -355,7 +440,7 @@ func CreatePullRequestReviewerFn(ctx context.Context, req mcp.CallToolRequest) (
var teamReviewers []string var teamReviewers []string
if teamReviewersArg, exists := req.GetArguments()["team_reviewers"]; exists { if teamReviewersArg, exists := req.GetArguments()["team_reviewers"]; exists {
if teamReviewersSlice, ok := teamReviewersArg.([]interface{}); ok { if teamReviewersSlice, ok := teamReviewersArg.([]any); ok {
for _, teamReviewer := range teamReviewersSlice { for _, teamReviewer := range teamReviewersSlice {
if teamReviewerStr, ok := teamReviewer.(string); ok { if teamReviewerStr, ok := teamReviewer.(string); ok {
teamReviewers = append(teamReviewers, teamReviewerStr) teamReviewers = append(teamReviewers, teamReviewerStr)
@@ -369,20 +454,20 @@ func CreatePullRequestReviewerFn(ctx context.Context, req mcp.CallToolRequest) (
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
} }
_, err = client.CreateReviewRequests(owner, repo, int64(index), gitea_sdk.PullReviewRequestOptions{ _, err = client.CreateReviewRequests(owner, repo, index, gitea_sdk.PullReviewRequestOptions{
Reviewers: reviewers, Reviewers: reviewers,
TeamReviewers: teamReviewers, TeamReviewers: teamReviewers,
}) })
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("create review requests for %v/%v/pr/%v err: %v", owner, repo, int64(index), err)) return to.ErrorResult(fmt.Errorf("create review requests for %v/%v/pr/%v err: %v", owner, repo, index, err))
} }
// Return a success message instead of the Response object which contains non-serializable functions // Return a success message instead of the Response object which contains non-serializable functions
successMsg := map[string]interface{}{ successMsg := map[string]any{
"message": "Successfully created review requests", "message": "Successfully created review requests",
"reviewers": reviewers, "reviewers": reviewers,
"team_reviewers": teamReviewers, "team_reviewers": teamReviewers,
"pr_index": int64(index), "pr_index": index,
"repository": fmt.Sprintf("%s/%s", owner, repo), "repository": fmt.Sprintf("%s/%s", owner, repo),
} }
@@ -393,20 +478,20 @@ func DeletePullRequestReviewerFn(ctx context.Context, req mcp.CallToolRequest) (
log.Debugf("Called DeletePullRequestReviewerFn") log.Debugf("Called DeletePullRequestReviewerFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
index, ok := req.GetArguments()["index"].(float64) index, err := params.GetIndex(req.GetArguments(), "index")
if !ok { if err != nil {
return to.ErrorResult(fmt.Errorf("index is required")) return to.ErrorResult(err)
} }
var reviewers []string var reviewers []string
if reviewersArg, exists := req.GetArguments()["reviewers"]; exists { if reviewersArg, exists := req.GetArguments()["reviewers"]; exists {
if reviewersSlice, ok := reviewersArg.([]interface{}); ok { if reviewersSlice, ok := reviewersArg.([]any); ok {
for _, reviewer := range reviewersSlice { for _, reviewer := range reviewersSlice {
if reviewerStr, ok := reviewer.(string); ok { if reviewerStr, ok := reviewer.(string); ok {
reviewers = append(reviewers, reviewerStr) reviewers = append(reviewers, reviewerStr)
@@ -417,7 +502,7 @@ func DeletePullRequestReviewerFn(ctx context.Context, req mcp.CallToolRequest) (
var teamReviewers []string var teamReviewers []string
if teamReviewersArg, exists := req.GetArguments()["team_reviewers"]; exists { if teamReviewersArg, exists := req.GetArguments()["team_reviewers"]; exists {
if teamReviewersSlice, ok := teamReviewersArg.([]interface{}); ok { if teamReviewersSlice, ok := teamReviewersArg.([]any); ok {
for _, teamReviewer := range teamReviewersSlice { for _, teamReviewer := range teamReviewersSlice {
if teamReviewerStr, ok := teamReviewer.(string); ok { if teamReviewerStr, ok := teamReviewer.(string); ok {
teamReviewers = append(teamReviewers, teamReviewerStr) teamReviewers = append(teamReviewers, teamReviewerStr)
@@ -431,19 +516,19 @@ func DeletePullRequestReviewerFn(ctx context.Context, req mcp.CallToolRequest) (
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
} }
_, err = client.DeleteReviewRequests(owner, repo, int64(index), gitea_sdk.PullReviewRequestOptions{ _, err = client.DeleteReviewRequests(owner, repo, index, gitea_sdk.PullReviewRequestOptions{
Reviewers: reviewers, Reviewers: reviewers,
TeamReviewers: teamReviewers, TeamReviewers: teamReviewers,
}) })
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("delete review requests for %v/%v/pr/%v err: %v", owner, repo, int64(index), err)) return to.ErrorResult(fmt.Errorf("delete review requests for %v/%v/pr/%v err: %v", owner, repo, index, err))
} }
successMsg := map[string]interface{}{ successMsg := map[string]any{
"message": "Successfully deleted review requests", "message": "Successfully deleted review requests",
"reviewers": reviewers, "reviewers": reviewers,
"team_reviewers": teamReviewers, "team_reviewers": teamReviewers,
"pr_index": int64(index), "pr_index": index,
"repository": fmt.Sprintf("%s/%s", owner, repo), "repository": fmt.Sprintf("%s/%s", owner, repo),
} }
@@ -454,38 +539,32 @@ func ListPullRequestReviewsFn(ctx context.Context, req mcp.CallToolRequest) (*mc
log.Debugf("Called ListPullRequestReviewsFn") log.Debugf("Called ListPullRequestReviewsFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
index, ok := req.GetArguments()["index"].(float64) index, err := params.GetIndex(req.GetArguments(), "index")
if !ok { if err != nil {
return to.ErrorResult(fmt.Errorf("index is required")) return to.ErrorResult(err)
}
page, ok := req.GetArguments()["page"].(float64)
if !ok {
page = 1
}
pageSize, ok := req.GetArguments()["pageSize"].(float64)
if !ok {
pageSize = 100
} }
page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
} }
reviews, _, err := client.ListPullReviews(owner, repo, int64(index), gitea_sdk.ListPullReviewsOptions{ reviews, _, err := client.ListPullReviews(owner, repo, index, gitea_sdk.ListPullReviewsOptions{
ListOptions: gitea_sdk.ListOptions{ ListOptions: gitea_sdk.ListOptions{
Page: int(page), Page: int(page),
PageSize: int(pageSize), PageSize: int(pageSize),
}, },
}) })
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("list reviews for %v/%v/pr/%v err: %v", owner, repo, int64(index), err)) return to.ErrorResult(fmt.Errorf("list reviews for %v/%v/pr/%v err: %v", owner, repo, index, err))
} }
return to.TextResult(reviews) return to.TextResult(reviews)
@@ -495,19 +574,19 @@ func GetPullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.
log.Debugf("Called GetPullRequestReviewFn") log.Debugf("Called GetPullRequestReviewFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
index, ok := req.GetArguments()["index"].(float64) index, err := params.GetIndex(req.GetArguments(), "index")
if !ok { if err != nil {
return to.ErrorResult(fmt.Errorf("index is required")) return to.ErrorResult(err)
} }
reviewID, ok := req.GetArguments()["review_id"].(float64) reviewID, err := params.GetIndex(req.GetArguments(), "review_id")
if !ok { if err != nil {
return to.ErrorResult(fmt.Errorf("review_id is required")) return to.ErrorResult(err)
} }
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
@@ -515,9 +594,9 @@ func GetPullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
} }
review, _, err := client.GetPullReview(owner, repo, int64(index), int64(reviewID)) review, _, err := client.GetPullReview(owner, repo, index, reviewID)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("get review %v for %v/%v/pr/%v err: %v", int64(reviewID), owner, repo, int64(index), err)) return to.ErrorResult(fmt.Errorf("get review %v for %v/%v/pr/%v err: %v", reviewID, owner, repo, index, err))
} }
return to.TextResult(review) return to.TextResult(review)
@@ -527,19 +606,19 @@ func ListPullRequestReviewCommentsFn(ctx context.Context, req mcp.CallToolReques
log.Debugf("Called ListPullRequestReviewCommentsFn") log.Debugf("Called ListPullRequestReviewCommentsFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
index, ok := req.GetArguments()["index"].(float64) index, err := params.GetIndex(req.GetArguments(), "index")
if !ok { if err != nil {
return to.ErrorResult(fmt.Errorf("index is required")) return to.ErrorResult(err)
} }
reviewID, ok := req.GetArguments()["review_id"].(float64) reviewID, err := params.GetIndex(req.GetArguments(), "review_id")
if !ok { if err != nil {
return to.ErrorResult(fmt.Errorf("review_id is required")) return to.ErrorResult(err)
} }
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
@@ -547,9 +626,9 @@ func ListPullRequestReviewCommentsFn(ctx context.Context, req mcp.CallToolReques
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
} }
comments, _, err := client.ListPullReviewComments(owner, repo, int64(index), int64(reviewID)) comments, _, err := client.ListPullReviewComments(owner, repo, index, reviewID)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("list review comments for review %v on %v/%v/pr/%v err: %v", int64(reviewID), owner, repo, int64(index), err)) return to.ErrorResult(fmt.Errorf("list review comments for review %v on %v/%v/pr/%v err: %v", reviewID, owner, repo, index, err))
} }
return to.TextResult(comments) return to.TextResult(comments)
@@ -559,15 +638,15 @@ func CreatePullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*m
log.Debugf("Called CreatePullRequestReviewFn") log.Debugf("Called CreatePullRequestReviewFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
index, ok := req.GetArguments()["index"].(float64) index, err := params.GetIndex(req.GetArguments(), "index")
if !ok { if err != nil {
return to.ErrorResult(fmt.Errorf("index is required")) return to.ErrorResult(err)
} }
opt := gitea_sdk.CreatePullReviewOptions{} opt := gitea_sdk.CreatePullReviewOptions{}
@@ -584,9 +663,9 @@ func CreatePullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*m
// Parse inline comments // Parse inline comments
if commentsArg, exists := req.GetArguments()["comments"]; exists { if commentsArg, exists := req.GetArguments()["comments"]; exists {
if commentsSlice, ok := commentsArg.([]interface{}); ok { if commentsSlice, ok := commentsArg.([]any); ok {
for _, comment := range commentsSlice { for _, comment := range commentsSlice {
if commentMap, ok := comment.(map[string]interface{}); ok { if commentMap, ok := comment.(map[string]any); ok {
reviewComment := gitea_sdk.CreatePullReviewComment{} reviewComment := gitea_sdk.CreatePullReviewComment{}
if path, ok := commentMap["path"].(string); ok { if path, ok := commentMap["path"].(string); ok {
reviewComment.Path = path reviewComment.Path = path
@@ -594,11 +673,11 @@ func CreatePullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*m
if body, ok := commentMap["body"].(string); ok { if body, ok := commentMap["body"].(string); ok {
reviewComment.Body = body reviewComment.Body = body
} }
if oldLineNum, ok := commentMap["old_line_num"].(float64); ok { if oldLineNum, ok := params.ToInt64(commentMap["old_line_num"]); ok {
reviewComment.OldLineNum = int64(oldLineNum) reviewComment.OldLineNum = oldLineNum
} }
if newLineNum, ok := commentMap["new_line_num"].(float64); ok { if newLineNum, ok := params.ToInt64(commentMap["new_line_num"]); ok {
reviewComment.NewLineNum = int64(newLineNum) reviewComment.NewLineNum = newLineNum
} }
opt.Comments = append(opt.Comments, reviewComment) opt.Comments = append(opt.Comments, reviewComment)
} }
@@ -611,9 +690,9 @@ func CreatePullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*m
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
} }
review, _, err := client.CreatePullReview(owner, repo, int64(index), opt) review, _, err := client.CreatePullReview(owner, repo, index, opt)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("create review for %v/%v/pr/%v err: %v", owner, repo, int64(index), err)) return to.ErrorResult(fmt.Errorf("create review for %v/%v/pr/%v err: %v", owner, repo, index, err))
} }
return to.TextResult(review) return to.TextResult(review)
@@ -623,23 +702,23 @@ func SubmitPullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*m
log.Debugf("Called SubmitPullRequestReviewFn") log.Debugf("Called SubmitPullRequestReviewFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
index, ok := req.GetArguments()["index"].(float64) index, err := params.GetIndex(req.GetArguments(), "index")
if !ok { if err != nil {
return to.ErrorResult(fmt.Errorf("index is required")) return to.ErrorResult(err)
} }
reviewID, ok := req.GetArguments()["review_id"].(float64) reviewID, err := params.GetIndex(req.GetArguments(), "review_id")
if !ok { if err != nil {
return to.ErrorResult(fmt.Errorf("review_id is required")) return to.ErrorResult(err)
} }
state, ok := req.GetArguments()["state"].(string) state, ok := req.GetArguments()["state"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("state is required")) return to.ErrorResult(errors.New("state is required"))
} }
opt := gitea_sdk.SubmitPullReviewOptions{ opt := gitea_sdk.SubmitPullReviewOptions{
@@ -654,9 +733,9 @@ func SubmitPullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*m
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
} }
review, _, err := client.SubmitPullReview(owner, repo, int64(index), int64(reviewID), opt) review, _, err := client.SubmitPullReview(owner, repo, index, reviewID, opt)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("submit review %v for %v/%v/pr/%v err: %v", int64(reviewID), owner, repo, int64(index), err)) return to.ErrorResult(fmt.Errorf("submit review %v for %v/%v/pr/%v err: %v", reviewID, owner, repo, index, err))
} }
return to.TextResult(review) return to.TextResult(review)
@@ -666,19 +745,19 @@ func DeletePullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*m
log.Debugf("Called DeletePullRequestReviewFn") log.Debugf("Called DeletePullRequestReviewFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
index, ok := req.GetArguments()["index"].(float64) index, err := params.GetIndex(req.GetArguments(), "index")
if !ok { if err != nil {
return to.ErrorResult(fmt.Errorf("index is required")) return to.ErrorResult(err)
} }
reviewID, ok := req.GetArguments()["review_id"].(float64) reviewID, err := params.GetIndex(req.GetArguments(), "review_id")
if !ok { if err != nil {
return to.ErrorResult(fmt.Errorf("review_id is required")) return to.ErrorResult(err)
} }
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
@@ -686,15 +765,15 @@ func DeletePullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*m
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
} }
_, err = client.DeletePullReview(owner, repo, int64(index), int64(reviewID)) _, err = client.DeletePullReview(owner, repo, index, reviewID)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("delete review %v for %v/%v/pr/%v err: %v", int64(reviewID), owner, repo, int64(index), err)) return to.ErrorResult(fmt.Errorf("delete review %v for %v/%v/pr/%v err: %v", reviewID, owner, repo, index, err))
} }
successMsg := map[string]interface{}{ successMsg := map[string]any{
"message": "Successfully deleted review", "message": "Successfully deleted review",
"review_id": int64(reviewID), "review_id": reviewID,
"pr_index": int64(index), "pr_index": index,
"repository": fmt.Sprintf("%s/%s", owner, repo), "repository": fmt.Sprintf("%s/%s", owner, repo),
} }
@@ -705,19 +784,19 @@ func DismissPullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*
log.Debugf("Called DismissPullRequestReviewFn") log.Debugf("Called DismissPullRequestReviewFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
index, ok := req.GetArguments()["index"].(float64) index, err := params.GetIndex(req.GetArguments(), "index")
if !ok { if err != nil {
return to.ErrorResult(fmt.Errorf("index is required")) return to.ErrorResult(err)
} }
reviewID, ok := req.GetArguments()["review_id"].(float64) reviewID, err := params.GetIndex(req.GetArguments(), "review_id")
if !ok { if err != nil {
return to.ErrorResult(fmt.Errorf("review_id is required")) return to.ErrorResult(err)
} }
opt := gitea_sdk.DismissPullReviewOptions{} opt := gitea_sdk.DismissPullReviewOptions{}
@@ -730,17 +809,153 @@ func DismissPullRequestReviewFn(ctx context.Context, req mcp.CallToolRequest) (*
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
} }
_, err = client.DismissPullReview(owner, repo, int64(index), int64(reviewID), opt) _, err = client.DismissPullReview(owner, repo, index, reviewID, opt)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("dismiss review %v for %v/%v/pr/%v err: %v", int64(reviewID), owner, repo, int64(index), err)) return to.ErrorResult(fmt.Errorf("dismiss review %v for %v/%v/pr/%v err: %v", reviewID, owner, repo, index, err))
} }
successMsg := map[string]interface{}{ successMsg := map[string]any{
"message": "Successfully dismissed review", "message": "Successfully dismissed review",
"review_id": int64(reviewID), "review_id": reviewID,
"pr_index": int64(index), "pr_index": index,
"repository": fmt.Sprintf("%s/%s", owner, repo), "repository": fmt.Sprintf("%s/%s", owner, repo),
} }
return to.TextResult(successMsg) return to.TextResult(successMsg)
} }
func MergePullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called MergePullRequestFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(errors.New("owner is required"))
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(errors.New("repo is required"))
}
index, err := params.GetIndex(req.GetArguments(), "index")
if err != nil {
return to.ErrorResult(err)
}
mergeStyle := "merge"
if style, exists := req.GetArguments()["merge_style"].(string); exists && style != "" {
mergeStyle = style
}
title := ""
if t, exists := req.GetArguments()["title"].(string); exists {
title = t
}
message := ""
if msg, exists := req.GetArguments()["message"].(string); exists {
message = msg
}
deleteBranch := false
if del, exists := req.GetArguments()["delete_branch"].(bool); exists {
deleteBranch = del
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
opt := gitea_sdk.MergePullRequestOption{
Style: gitea_sdk.MergeStyle(mergeStyle),
Title: title,
Message: message,
DeleteBranchAfterMerge: deleteBranch,
}
merged, resp, err := client.MergePullRequest(owner, repo, index, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("merge %v/%v/pr/%v err: %v", owner, repo, index, err))
}
if !merged && resp != nil && resp.StatusCode >= 400 {
return to.ErrorResult(fmt.Errorf("merge %v/%v/pr/%v failed: HTTP %d %s", owner, repo, index, resp.StatusCode, resp.Status))
}
if !merged {
return to.ErrorResult(fmt.Errorf("merge %v/%v/pr/%v returned merged=false", owner, repo, index))
}
successMsg := map[string]any{
"merged": merged,
"pr_index": index,
"repository": fmt.Sprintf("%s/%s", owner, repo),
"merge_style": mergeStyle,
"branch_deleted": deleteBranch,
}
return to.TextResult(successMsg)
}
func EditPullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called EditPullRequestFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(errors.New("owner is required"))
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(errors.New("repo is required"))
}
index, err := params.GetIndex(req.GetArguments(), "index")
if err != nil {
return to.ErrorResult(err)
}
opt := gitea_sdk.EditPullRequestOption{}
if title, ok := req.GetArguments()["title"].(string); ok {
opt.Title = title
}
if body, ok := req.GetArguments()["body"].(string); ok {
opt.Body = new(body)
}
if base, ok := req.GetArguments()["base"].(string); ok {
opt.Base = base
}
if assignee, ok := req.GetArguments()["assignee"].(string); ok {
opt.Assignee = assignee
}
if assigneesArg, exists := req.GetArguments()["assignees"]; exists {
if assigneesSlice, ok := assigneesArg.([]any); ok {
var assignees []string
for _, a := range assigneesSlice {
if s, ok := a.(string); ok {
assignees = append(assignees, s)
}
}
opt.Assignees = assignees
}
}
if val, exists := req.GetArguments()["milestone"]; exists {
if milestone, ok := params.ToInt64(val); ok {
opt.Milestone = milestone
}
}
if state, ok := req.GetArguments()["state"].(string); ok {
opt.State = new(gitea_sdk.StateType(state))
}
if allowMaintainerEdit, ok := req.GetArguments()["allow_maintainer_edit"].(bool); ok {
opt.AllowMaintainerEdit = new(allowMaintainerEdit)
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
pr, _, err := client.EditPullRequest(owner, repo, index, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("edit %v/%v/pr/%v err: %v", owner, repo, index, err))
}
return to.TextResult(pr)
}

397
operation/pull/pull_test.go Normal file
View File

@@ -0,0 +1,397 @@
package pull
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"sync"
"testing"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/flag"
"github.com/mark3labs/mcp-go/mcp"
)
func TestEditPullRequestFn(t *testing.T) {
const (
owner = "octo"
repo = "demo"
index = 7
)
indexInputs := []struct {
name string
val any
}{
{"float64", float64(index)},
{"string", "7"},
}
for _, ii := range indexInputs {
t.Run(ii.name, func(t *testing.T) {
var (
mu sync.Mutex
gotMethod string
gotPath string
gotBody map[string]any
)
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/v1/version":
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"version":"1.12.0"}`))
case fmt.Sprintf("/api/v1/repos/%s/%s", owner, repo):
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"private":false}`))
case fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d", owner, repo, index):
mu.Lock()
gotMethod = r.Method
gotPath = r.URL.Path
var body map[string]any
_ = json.NewDecoder(r.Body).Decode(&body)
gotBody = body
mu.Unlock()
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(fmt.Appendf(nil, `{"number":%d,"title":"%s","state":"open"}`, index, body["title"]))
default:
http.NotFound(w, r)
}
})
server := httptest.NewServer(handler)
defer server.Close()
origHost := flag.Host
origToken := flag.Token
origVersion := flag.Version
flag.Host = server.URL
flag.Token = ""
flag.Version = "test"
defer func() {
flag.Host = origHost
flag.Token = origToken
flag.Version = origVersion
}()
req := mcp.CallToolRequest{
Params: mcp.CallToolParams{
Arguments: map[string]any{
"owner": owner,
"repo": repo,
"index": ii.val,
"title": "WIP: my feature",
"state": "open",
},
},
}
result, err := EditPullRequestFn(context.Background(), req)
if err != nil {
t.Fatalf("EditPullRequestFn() error = %v", err)
}
mu.Lock()
defer mu.Unlock()
if gotMethod != http.MethodPatch {
t.Fatalf("expected PATCH request, got %s", gotMethod)
}
if gotPath != fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d", owner, repo, index) {
t.Fatalf("unexpected path: %s", gotPath)
}
if gotBody["title"] != "WIP: my feature" {
t.Fatalf("expected title 'WIP: my feature', got %v", gotBody["title"])
}
if gotBody["state"] != "open" {
t.Fatalf("expected state 'open', got %v", gotBody["state"])
}
if len(result.Content) == 0 {
t.Fatalf("expected content in result")
}
textContent, ok := mcp.AsTextContent(result.Content[0])
if !ok {
t.Fatalf("expected text content, got %T", result.Content[0])
}
var parsed struct {
Result map[string]any `json:"Result"`
}
if err := json.Unmarshal([]byte(textContent.Text), &parsed); err != nil {
t.Fatalf("unmarshal result text: %v", err)
}
if got := parsed.Result["title"].(string); got != "WIP: my feature" {
t.Fatalf("result title = %q, want %q", got, "WIP: my feature")
}
})
}
}
func TestMergePullRequestFn(t *testing.T) {
const (
owner = "octo"
repo = "demo"
index = 5
)
indexInputs := []struct {
name string
val any
}{
{"float64", float64(index)},
{"string", "5"},
}
for _, ii := range indexInputs {
t.Run(ii.name, func(t *testing.T) {
var (
mu sync.Mutex
gotMethod string
gotPath string
gotBody map[string]any
)
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/v1/version":
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"version":"1.12.0"}`))
case fmt.Sprintf("/api/v1/repos/%s/%s", owner, repo):
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"private":false}`))
case fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/merge", owner, repo, index):
mu.Lock()
gotMethod = r.Method
gotPath = r.URL.Path
var body map[string]any
_ = json.NewDecoder(r.Body).Decode(&body)
gotBody = body
mu.Unlock()
w.WriteHeader(http.StatusOK)
default:
http.NotFound(w, r)
}
})
server := httptest.NewServer(handler)
defer server.Close()
origHost := flag.Host
origToken := flag.Token
origVersion := flag.Version
flag.Host = server.URL
flag.Token = ""
flag.Version = "test"
defer func() {
flag.Host = origHost
flag.Token = origToken
flag.Version = origVersion
}()
req := mcp.CallToolRequest{
Params: mcp.CallToolParams{
Arguments: map[string]any{
"owner": owner,
"repo": repo,
"index": ii.val,
"merge_style": "squash",
"title": "feat: my squashed commit",
"message": "Squash merge of PR #5",
"delete_branch": true,
},
},
}
result, err := MergePullRequestFn(context.Background(), req)
if err != nil {
t.Fatalf("MergePullRequestFn() error = %v", err)
}
mu.Lock()
defer mu.Unlock()
if gotMethod != http.MethodPost {
t.Fatalf("expected POST request, got %s", gotMethod)
}
if gotPath != fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/merge", owner, repo, index) {
t.Fatalf("unexpected path: %s", gotPath)
}
if gotBody["Do"] != "squash" {
t.Fatalf("expected Do 'squash', got %v", gotBody["Do"])
}
if gotBody["MergeTitleField"] != "feat: my squashed commit" {
t.Fatalf("expected MergeTitleField 'feat: my squashed commit', got %v", gotBody["MergeTitleField"])
}
if gotBody["MergeMessageField"] != "Squash merge of PR #5" {
t.Fatalf("expected MergeMessageField 'Squash merge of PR #5', got %v", gotBody["MergeMessageField"])
}
if gotBody["delete_branch_after_merge"] != true {
t.Fatalf("expected delete_branch_after_merge true, got %v", gotBody["delete_branch_after_merge"])
}
if len(result.Content) == 0 {
t.Fatalf("expected content in result")
}
textContent, ok := mcp.AsTextContent(result.Content[0])
if !ok {
t.Fatalf("expected text content, got %T", result.Content[0])
}
var parsed struct {
Result map[string]any `json:"Result"`
}
if err := json.Unmarshal([]byte(textContent.Text), &parsed); err != nil {
t.Fatalf("unmarshal result text: %v", err)
}
if parsed.Result["merged"] != true {
t.Fatalf("expected merged=true, got %v", parsed.Result["merged"])
}
if parsed.Result["merge_style"] != "squash" {
t.Fatalf("expected merge_style 'squash', got %v", parsed.Result["merge_style"])
}
if parsed.Result["branch_deleted"] != true {
t.Fatalf("expected branch_deleted=true, got %v", parsed.Result["branch_deleted"])
}
})
}
}
func TestGetPullRequestDiffFn(t *testing.T) {
const (
owner = "octo"
repo = "demo"
index = 12
diffRaw = "diff --git a/file.txt b/file.txt\n+line\n"
)
indexInputs := []struct {
name string
val any
}{
{"float64", float64(index)},
{"string", "12"},
}
for _, ii := range indexInputs {
t.Run(ii.name, func(t *testing.T) {
var (
mu sync.Mutex
diffRequested bool
binaryValue string
)
errCh := make(chan error, 1)
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/v1/version":
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"version":"1.12.0"}`))
case fmt.Sprintf("/api/v1/repos/%s/%s", owner, repo):
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"private":false}`))
case fmt.Sprintf("/%s/%s/pulls/%d.diff", owner, repo, index):
if r.Method != http.MethodGet {
select {
case errCh <- fmt.Errorf("unexpected method: %s", r.Method):
default:
}
}
mu.Lock()
diffRequested = true
binaryValue = r.URL.Query().Get("binary")
mu.Unlock()
w.Header().Set("Content-Type", "text/plain")
_, _ = w.Write([]byte(diffRaw))
default:
select {
case errCh <- fmt.Errorf("unexpected request path: %s", r.URL.Path):
default:
}
}
})
server := httptest.NewServer(handler)
defer server.Close()
origHost := flag.Host
origToken := flag.Token
origVersion := flag.Version
flag.Host = server.URL
flag.Token = ""
flag.Version = "test"
defer func() {
flag.Host = origHost
flag.Token = origToken
flag.Version = origVersion
}()
req := mcp.CallToolRequest{
Params: mcp.CallToolParams{
Arguments: map[string]any{
"owner": owner,
"repo": repo,
"index": ii.val,
"binary": true,
},
},
}
result, err := GetPullRequestDiffFn(context.Background(), req)
if err != nil {
t.Fatalf("GetPullRequestDiffFn() error = %v", err)
}
select {
case reqErr := <-errCh:
t.Fatalf("handler error: %v", reqErr)
default:
}
mu.Lock()
requested := diffRequested
gotBinary := binaryValue
mu.Unlock()
if !requested {
t.Fatalf("expected diff request to be made")
}
if gotBinary != "true" {
t.Fatalf("expected binary=true query param, got %q", gotBinary)
}
if len(result.Content) == 0 {
t.Fatalf("expected content in result")
}
textContent, ok := mcp.AsTextContent(result.Content[0])
if !ok {
t.Fatalf("expected text content, got %T", result.Content[0])
}
var parsed struct {
Result map[string]any `json:"Result"`
}
if err := json.Unmarshal([]byte(textContent.Text), &parsed); err != nil {
t.Fatalf("unmarshal result text: %v", err)
}
if got, ok := parsed.Result["diff"].(string); !ok || got != diffRaw {
t.Fatalf("diff = %q, want %q", got, diffRaw)
}
if got, ok := parsed.Result["binary"].(bool); !ok || got != true {
t.Fatalf("binary = %v, want true", got)
}
if got, ok := parsed.Result["index"].(float64); !ok || int64(got) != int64(index) {
t.Fatalf("index = %v, want %d", got, index)
}
if got, ok := parsed.Result["owner"].(string); !ok || got != owner {
t.Fatalf("owner = %q, want %q", got, owner)
}
if got, ok := parsed.Result["repo"].(string); !ok || got != repo {
t.Fatalf("repo = %q, want %q", got, repo)
}
})
}
}

View File

@@ -2,11 +2,12 @@ package repo
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"gitea.com/gitea/gitea-mcp/pkg/gitea" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/gitea"
"gitea.com/gitea/gitea-mcp/pkg/log" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/log"
"gitea.com/gitea/gitea-mcp/pkg/to" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/to"
gitea_sdk "code.gitea.io/sdk/gitea" gitea_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/mcp"
@@ -64,15 +65,15 @@ func CreateBranchFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTool
log.Debugf("Called CreateBranchFn") log.Debugf("Called CreateBranchFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
branch, ok := req.GetArguments()["branch"].(string) branch, ok := req.GetArguments()["branch"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("branch is required")) return to.ErrorResult(errors.New("branch is required"))
} }
oldBranch, _ := req.GetArguments()["old_branch"].(string) oldBranch, _ := req.GetArguments()["old_branch"].(string)
@@ -95,15 +96,15 @@ func DeleteBranchFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTool
log.Debugf("Called DeleteBranchFn") log.Debugf("Called DeleteBranchFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
branch, ok := req.GetArguments()["branch"].(string) branch, ok := req.GetArguments()["branch"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("branch is required")) return to.ErrorResult(errors.New("branch is required"))
} }
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
if err != nil { if err != nil {
@@ -121,11 +122,11 @@ func ListBranchesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTool
log.Debugf("Called ListBranchesFn") log.Debugf("Called ListBranchesFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
opt := gitea_sdk.ListRepoBranchesOptions{ opt := gitea_sdk.ListRepoBranchesOptions{
ListOptions: gitea_sdk.ListOptions{ ListOptions: gitea_sdk.ListOptions{

View File

@@ -0,0 +1,226 @@
package repo
import (
"context"
"errors"
"fmt"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/gitea"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/log"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/params"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/to"
gitea_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
const (
ListBranchProtectionsToolName = "list_branch_protections"
GetBranchProtectionToolName = "get_branch_protection"
CreateBranchProtectionToolName = "create_branch_protection"
EditBranchProtectionToolName = "edit_branch_protection"
DeleteBranchProtectionToolName = "delete_branch_protection"
)
var (
ListBranchProtectionsTool = mcp.NewTool(
ListBranchProtectionsToolName,
mcp.WithDescription("List branch protections for a repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)),
)
GetBranchProtectionTool = mcp.NewTool(
GetBranchProtectionToolName,
mcp.WithDescription("Get a branch protection rule by name"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("name", mcp.Required(), mcp.Description("branch protection rule name")),
)
CreateBranchProtectionTool = mcp.NewTool(
CreateBranchProtectionToolName,
mcp.WithDescription("Create a branch protection rule"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("rule_name", mcp.Required(), mcp.Description("rule name (glob pattern for branch matching)")),
mcp.WithBoolean("enable_push", mcp.Description("enable push to protected branch")),
mcp.WithBoolean("enable_merge_whitelist", mcp.Description("enable merge whitelist")),
mcp.WithBoolean("enable_status_check", mcp.Description("enable status check")),
mcp.WithNumber("required_approvals", mcp.Description("number of required approvals")),
mcp.WithBoolean("block_on_rejected_reviews", mcp.Description("block merge on rejected reviews")),
mcp.WithBoolean("dismiss_stale_approvals", mcp.Description("dismiss stale approvals on new commits")),
)
EditBranchProtectionTool = mcp.NewTool(
EditBranchProtectionToolName,
mcp.WithDescription("Edit a branch protection rule"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("name", mcp.Required(), mcp.Description("branch protection rule name")),
mcp.WithBoolean("enable_push", mcp.Description("enable push to protected branch")),
mcp.WithBoolean("enable_merge_whitelist", mcp.Description("enable merge whitelist")),
mcp.WithBoolean("enable_status_check", mcp.Description("enable status check")),
mcp.WithNumber("required_approvals", mcp.Description("number of required approvals")),
mcp.WithBoolean("block_on_rejected_reviews", mcp.Description("block merge on rejected reviews")),
mcp.WithBoolean("dismiss_stale_approvals", mcp.Description("dismiss stale approvals on new commits")),
)
DeleteBranchProtectionTool = mcp.NewTool(
DeleteBranchProtectionToolName,
mcp.WithDescription("Delete a branch protection rule"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("name", mcp.Required(), mcp.Description("branch protection rule name")),
)
)
func init() {
Tool.RegisterRead(server.ServerTool{Tool: ListBranchProtectionsTool, Handler: ListBranchProtectionsFn})
Tool.RegisterRead(server.ServerTool{Tool: GetBranchProtectionTool, Handler: GetBranchProtectionFn})
Tool.RegisterWrite(server.ServerTool{Tool: CreateBranchProtectionTool, Handler: CreateBranchProtectionFn})
Tool.RegisterWrite(server.ServerTool{Tool: EditBranchProtectionTool, Handler: EditBranchProtectionFn})
Tool.RegisterWrite(server.ServerTool{Tool: DeleteBranchProtectionTool, Handler: DeleteBranchProtectionFn})
}
func ListBranchProtectionsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListBranchProtectionsFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
bps, _, err := client.ListBranchProtections(owner, repo, gitea_sdk.ListBranchProtectionsOptions{
ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)},
})
if err != nil {
return to.ErrorResult(fmt.Errorf("list branch protections err: %v", err))
}
return to.TextResult(bps)
}
func GetBranchProtectionFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetBranchProtectionFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
name, _ := req.GetArguments()["name"].(string)
if owner == "" || repo == "" || name == "" {
return to.ErrorResult(errors.New("owner, repo, and name are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
bp, _, err := client.GetBranchProtection(owner, repo, name)
if err != nil {
return to.ErrorResult(fmt.Errorf("get branch protection err: %v", err))
}
return to.TextResult(bp)
}
func CreateBranchProtectionFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CreateBranchProtectionFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
ruleName, _ := req.GetArguments()["rule_name"].(string)
if owner == "" || repo == "" || ruleName == "" {
return to.ErrorResult(errors.New("owner, repo, and rule_name are required"))
}
opt := gitea_sdk.CreateBranchProtectionOption{
RuleName: ruleName,
}
if v, ok := req.GetArguments()["enable_push"].(bool); ok {
opt.EnablePush = v
}
if v, ok := req.GetArguments()["enable_merge_whitelist"].(bool); ok {
opt.EnableMergeWhitelist = v
}
if v, ok := req.GetArguments()["enable_status_check"].(bool); ok {
opt.EnableStatusCheck = v
}
if v, ok := req.GetArguments()["required_approvals"].(float64); ok {
opt.RequiredApprovals = int64(v)
}
if v, ok := req.GetArguments()["block_on_rejected_reviews"].(bool); ok {
opt.BlockOnRejectedReviews = v
}
if v, ok := req.GetArguments()["dismiss_stale_approvals"].(bool); ok {
opt.DismissStaleApprovals = v
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
bp, _, err := client.CreateBranchProtection(owner, repo, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("create branch protection err: %v", err))
}
return to.TextResult(bp)
}
func EditBranchProtectionFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called EditBranchProtectionFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
name, _ := req.GetArguments()["name"].(string)
if owner == "" || repo == "" || name == "" {
return to.ErrorResult(errors.New("owner, repo, and name are required"))
}
opt := gitea_sdk.EditBranchProtectionOption{}
if v, ok := req.GetArguments()["enable_push"].(bool); ok {
opt.EnablePush = &v
}
if v, ok := req.GetArguments()["enable_merge_whitelist"].(bool); ok {
opt.EnableMergeWhitelist = &v
}
if v, ok := req.GetArguments()["enable_status_check"].(bool); ok {
opt.EnableStatusCheck = &v
}
if v, ok := req.GetArguments()["required_approvals"].(float64); ok {
approvals := int64(v)
opt.RequiredApprovals = &approvals
}
if v, ok := req.GetArguments()["block_on_rejected_reviews"].(bool); ok {
opt.BlockOnRejectedReviews = &v
}
if v, ok := req.GetArguments()["dismiss_stale_approvals"].(bool); ok {
opt.DismissStaleApprovals = &v
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
bp, _, err := client.EditBranchProtection(owner, repo, name, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("edit branch protection err: %v", err))
}
return to.TextResult(bp)
}
func DeleteBranchProtectionFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called DeleteBranchProtectionFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
name, _ := req.GetArguments()["name"].(string)
if owner == "" || repo == "" || name == "" {
return to.ErrorResult(errors.New("owner, repo, and name are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.DeleteBranchProtection(owner, repo, name)
if err != nil {
return to.ErrorResult(fmt.Errorf("delete branch protection err: %v", err))
}
return to.TextResult(map[string]string{"status": "deleted"})
}

View File

@@ -0,0 +1,233 @@
package repo
import (
"context"
"errors"
"fmt"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/gitea"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/log"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/params"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/to"
gitea_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
const (
ListCollaboratorsToolName = "list_collaborators"
IsCollaboratorToolName = "is_collaborator"
CollaboratorPermissionToolName = "collaborator_permission"
AddCollaboratorToolName = "add_collaborator"
DeleteCollaboratorToolName = "delete_collaborator"
GetReviewersToolName = "get_reviewers"
GetAssigneesToolName = "get_assignees"
)
var (
ListCollaboratorsTool = mcp.NewTool(
ListCollaboratorsToolName,
mcp.WithDescription("List a repository's collaborators"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)),
)
IsCollaboratorTool = mcp.NewTool(
IsCollaboratorToolName,
mcp.WithDescription("Check if a user is a collaborator of a repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("collaborator", mcp.Required(), mcp.Description("username to check")),
)
CollaboratorPermissionTool = mcp.NewTool(
CollaboratorPermissionToolName,
mcp.WithDescription("Get a collaborator's permission level on a repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("collaborator", mcp.Required(), mcp.Description("collaborator username")),
)
AddCollaboratorTool = mcp.NewTool(
AddCollaboratorToolName,
mcp.WithDescription("Add a collaborator to a repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("collaborator", mcp.Required(), mcp.Description("username to add")),
mcp.WithString("permission", mcp.Description("permission level: read, write, admin (default: write)")),
)
DeleteCollaboratorTool = mcp.NewTool(
DeleteCollaboratorToolName,
mcp.WithDescription("Remove a collaborator from a repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("collaborator", mcp.Required(), mcp.Description("username to remove")),
)
GetReviewersTool = mcp.NewTool(
GetReviewersToolName,
mcp.WithDescription("Get the list of users who can review PRs in a repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
)
GetAssigneesTool = mcp.NewTool(
GetAssigneesToolName,
mcp.WithDescription("Get the list of users who can be assigned to issues in a repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
)
)
func init() {
Tool.RegisterRead(server.ServerTool{Tool: ListCollaboratorsTool, Handler: ListCollaboratorsFn})
Tool.RegisterRead(server.ServerTool{Tool: IsCollaboratorTool, Handler: IsCollaboratorFn})
Tool.RegisterRead(server.ServerTool{Tool: CollaboratorPermissionTool, Handler: CollaboratorPermissionFn})
Tool.RegisterRead(server.ServerTool{Tool: GetReviewersTool, Handler: GetReviewersFn})
Tool.RegisterRead(server.ServerTool{Tool: GetAssigneesTool, Handler: GetAssigneesFn})
Tool.RegisterWrite(server.ServerTool{Tool: AddCollaboratorTool, Handler: AddCollaboratorFn})
Tool.RegisterWrite(server.ServerTool{Tool: DeleteCollaboratorTool, Handler: DeleteCollaboratorFn})
}
func ListCollaboratorsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListCollaboratorsFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
users, _, err := client.ListCollaborators(owner, repo, gitea_sdk.ListCollaboratorsOptions{
ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)},
})
if err != nil {
return to.ErrorResult(fmt.Errorf("list collaborators err: %v", err))
}
return to.TextResult(users)
}
func IsCollaboratorFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called IsCollaboratorFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
collaborator, _ := req.GetArguments()["collaborator"].(string)
if owner == "" || repo == "" || collaborator == "" {
return to.ErrorResult(errors.New("owner, repo, and collaborator are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
isCollab, _, err := client.IsCollaborator(owner, repo, collaborator)
if err != nil {
return to.ErrorResult(fmt.Errorf("check collaborator err: %v", err))
}
return to.TextResult(map[string]any{"collaborator": collaborator, "is_collaborator": isCollab})
}
func CollaboratorPermissionFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CollaboratorPermissionFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
collaborator, _ := req.GetArguments()["collaborator"].(string)
if owner == "" || repo == "" || collaborator == "" {
return to.ErrorResult(errors.New("owner, repo, and collaborator are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
perm, _, err := client.CollaboratorPermission(owner, repo, collaborator)
if err != nil {
return to.ErrorResult(fmt.Errorf("get collaborator permission err: %v", err))
}
return to.TextResult(perm)
}
func AddCollaboratorFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called AddCollaboratorFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
collaborator, _ := req.GetArguments()["collaborator"].(string)
if owner == "" || repo == "" || collaborator == "" {
return to.ErrorResult(errors.New("owner, repo, and collaborator are required"))
}
opt := gitea_sdk.AddCollaboratorOption{}
if v, ok := req.GetArguments()["permission"].(string); ok {
perm := gitea_sdk.AccessMode(v)
opt.Permission = &perm
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.AddCollaborator(owner, repo, collaborator, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("add collaborator err: %v", err))
}
return to.TextResult(map[string]string{"status": "added", "collaborator": collaborator})
}
func DeleteCollaboratorFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called DeleteCollaboratorFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
collaborator, _ := req.GetArguments()["collaborator"].(string)
if owner == "" || repo == "" || collaborator == "" {
return to.ErrorResult(errors.New("owner, repo, and collaborator are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.DeleteCollaborator(owner, repo, collaborator)
if err != nil {
return to.ErrorResult(fmt.Errorf("delete collaborator err: %v", err))
}
return to.TextResult(map[string]string{"status": "removed", "collaborator": collaborator})
}
func GetReviewersFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetReviewersFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
users, _, err := client.GetReviewers(owner, repo)
if err != nil {
return to.ErrorResult(fmt.Errorf("get reviewers err: %v", err))
}
return to.TextResult(users)
}
func GetAssigneesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetAssigneesFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
users, _, err := client.GetAssignees(owner, repo)
if err != nil {
return to.ErrorResult(fmt.Errorf("get assignees err: %v", err))
}
return to.TextResult(users)
}

View File

@@ -2,11 +2,13 @@ package repo
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"gitea.com/gitea/gitea-mcp/pkg/gitea" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/gitea"
"gitea.com/gitea/gitea-mcp/pkg/log" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/log"
"gitea.com/gitea/gitea-mcp/pkg/to" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/params"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/to"
gitea_sdk "code.gitea.io/sdk/gitea" gitea_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/mcp"
@@ -39,19 +41,19 @@ func ListRepoCommitsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallT
log.Debugf("Called ListRepoCommitsFn") log.Debugf("Called ListRepoCommitsFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
page, ok := req.GetArguments()["page"].(float64) page, err := params.GetIndex(req.GetArguments(), "page")
if !ok { if err != nil {
return to.ErrorResult(fmt.Errorf("page is required")) return to.ErrorResult(err)
} }
pageSize, ok := req.GetArguments()["page_size"].(float64) pageSize, err := params.GetIndex(req.GetArguments(), "page_size")
if !ok { if err != nil {
return to.ErrorResult(fmt.Errorf("page_size is required")) return to.ErrorResult(err)
} }
sha, _ := req.GetArguments()["sha"].(string) sha, _ := req.GetArguments()["sha"].(string)
path, _ := req.GetArguments()["path"].(string) path, _ := req.GetArguments()["path"].(string)

View File

@@ -0,0 +1,161 @@
package repo
import (
"context"
"errors"
"fmt"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/gitea"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/log"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/params"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/to"
gitea_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
const (
ListDeployKeysToolName = "list_deploy_keys"
GetDeployKeyToolName = "get_deploy_key"
CreateDeployKeyToolName = "create_deploy_key"
DeleteDeployKeyToolName = "delete_deploy_key"
)
var (
ListDeployKeysTool = mcp.NewTool(
ListDeployKeysToolName,
mcp.WithDescription("List a repository's deploy keys"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)),
)
GetDeployKeyTool = mcp.NewTool(
GetDeployKeyToolName,
mcp.WithDescription("Get a deploy key by ID"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("key_id", mcp.Required(), mcp.Description("deploy key ID")),
)
CreateDeployKeyTool = mcp.NewTool(
CreateDeployKeyToolName,
mcp.WithDescription("Add a deploy key to a repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("title", mcp.Required(), mcp.Description("key title")),
mcp.WithString("key", mcp.Required(), mcp.Description("public key content")),
mcp.WithBoolean("read_only", mcp.Description("whether the key has read-only access (default: true)")),
)
DeleteDeployKeyTool = mcp.NewTool(
DeleteDeployKeyToolName,
mcp.WithDescription("Remove a deploy key from a repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("key_id", mcp.Required(), mcp.Description("deploy key ID")),
)
)
func init() {
Tool.RegisterRead(server.ServerTool{Tool: ListDeployKeysTool, Handler: ListDeployKeysFn})
Tool.RegisterRead(server.ServerTool{Tool: GetDeployKeyTool, Handler: GetDeployKeyFn})
Tool.RegisterWrite(server.ServerTool{Tool: CreateDeployKeyTool, Handler: CreateDeployKeyFn})
Tool.RegisterWrite(server.ServerTool{Tool: DeleteDeployKeyTool, Handler: DeleteDeployKeyFn})
}
func ListDeployKeysFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListDeployKeysFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
keys, _, err := client.ListDeployKeys(owner, repo, gitea_sdk.ListDeployKeysOptions{
ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)},
})
if err != nil {
return to.ErrorResult(fmt.Errorf("list deploy keys err: %v", err))
}
return to.TextResult(keys)
}
func GetDeployKeyFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetDeployKeyFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
keyID, err := params.GetIndex(req.GetArguments(), "key_id")
if err != nil {
return to.ErrorResult(err)
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
key, _, err := client.GetDeployKey(owner, repo, keyID)
if err != nil {
return to.ErrorResult(fmt.Errorf("get deploy key err: %v", err))
}
return to.TextResult(key)
}
func CreateDeployKeyFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CreateDeployKeyFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
title, _ := req.GetArguments()["title"].(string)
key, _ := req.GetArguments()["key"].(string)
if owner == "" || repo == "" || title == "" || key == "" {
return to.ErrorResult(errors.New("owner, repo, title, and key are required"))
}
opt := gitea_sdk.CreateKeyOption{
Title: title,
Key: key,
ReadOnly: true,
}
if v, ok := req.GetArguments()["read_only"].(bool); ok {
opt.ReadOnly = v
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
dk, _, err := client.CreateDeployKey(owner, repo, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("create deploy key err: %v", err))
}
return to.TextResult(dk)
}
func DeleteDeployKeyFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called DeleteDeployKeyFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
keyID, err := params.GetIndex(req.GetArguments(), "key_id")
if err != nil {
return to.ErrorResult(err)
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.DeleteDeployKey(owner, repo, keyID)
if err != nil {
return to.ErrorResult(fmt.Errorf("delete deploy key err: %v", err))
}
return to.TextResult(map[string]string{"status": "deleted"})
}

View File

@@ -6,11 +6,12 @@ import (
"context" "context"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"gitea.com/gitea/gitea-mcp/pkg/gitea" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/gitea"
"gitea.com/gitea/gitea-mcp/pkg/log" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/log"
"gitea.com/gitea/gitea-mcp/pkg/to" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/to"
gitea_sdk "code.gitea.io/sdk/gitea" gitea_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/mcp"
@@ -113,16 +114,16 @@ func GetFileContentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo
log.Debugf("Called GetFileFn") log.Debugf("Called GetFileFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
ref, _ := req.GetArguments()["ref"].(string) ref, _ := req.GetArguments()["ref"].(string)
filePath, ok := req.GetArguments()["filePath"].(string) filePath, ok := req.GetArguments()["filePath"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("filePath is required")) return to.ErrorResult(errors.New("filePath is required"))
} }
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
if err != nil { if err != nil {
@@ -151,7 +152,6 @@ func GetFileContentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo
LineNumber: line, LineNumber: line,
Content: scanner.Text(), Content: scanner.Text(),
}) })
} }
if err := scanner.Err(); err != nil { if err := scanner.Err(); err != nil {
return to.ErrorResult(fmt.Errorf("scan content err: %v", err)) return to.ErrorResult(fmt.Errorf("scan content err: %v", err))
@@ -177,16 +177,16 @@ func GetDirContentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToo
log.Debugf("Called GetDirContentFn") log.Debugf("Called GetDirContentFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
ref, _ := req.GetArguments()["ref"].(string) ref, _ := req.GetArguments()["ref"].(string)
filePath, ok := req.GetArguments()["filePath"].(string) filePath, ok := req.GetArguments()["filePath"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("filePath is required")) return to.ErrorResult(errors.New("filePath is required"))
} }
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
if err != nil { if err != nil {
@@ -203,15 +203,15 @@ func CreateFileFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRe
log.Debugf("Called CreateFileFn") log.Debugf("Called CreateFileFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
filePath, ok := req.GetArguments()["filePath"].(string) filePath, ok := req.GetArguments()["filePath"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("filePath is required")) return to.ErrorResult(errors.New("filePath is required"))
} }
content, _ := req.GetArguments()["content"].(string) content, _ := req.GetArguments()["content"].(string)
message, _ := req.GetArguments()["message"].(string) message, _ := req.GetArguments()["message"].(string)
@@ -239,19 +239,19 @@ func UpdateFileFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRe
log.Debugf("Called UpdateFileFn") log.Debugf("Called UpdateFileFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
filePath, ok := req.GetArguments()["filePath"].(string) filePath, ok := req.GetArguments()["filePath"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("filePath is required")) return to.ErrorResult(errors.New("filePath is required"))
} }
sha, ok := req.GetArguments()["sha"].(string) sha, ok := req.GetArguments()["sha"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("sha is required")) return to.ErrorResult(errors.New("sha is required"))
} }
content, _ := req.GetArguments()["content"].(string) content, _ := req.GetArguments()["content"].(string)
message, _ := req.GetArguments()["message"].(string) message, _ := req.GetArguments()["message"].(string)
@@ -280,21 +280,21 @@ func DeleteFileFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRe
log.Debugf("Called DeleteFileFn") log.Debugf("Called DeleteFileFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
filePath, ok := req.GetArguments()["filePath"].(string) filePath, ok := req.GetArguments()["filePath"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("filePath is required")) return to.ErrorResult(errors.New("filePath is required"))
} }
message, _ := req.GetArguments()["message"].(string) message, _ := req.GetArguments()["message"].(string)
branchName, _ := req.GetArguments()["branch_name"].(string) branchName, _ := req.GetArguments()["branch_name"].(string)
sha, ok := req.GetArguments()["sha"].(string) sha, ok := req.GetArguments()["sha"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("sha is required")) return to.ErrorResult(errors.New("sha is required"))
} }
opt := gitea_sdk.DeleteFileOptions{ opt := gitea_sdk.DeleteFileOptions{
FileOptions: gitea_sdk.FileOptions{ FileOptions: gitea_sdk.FileOptions{

175
operation/repo/git.go Normal file
View File

@@ -0,0 +1,175 @@
package repo
import (
"context"
"errors"
"fmt"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/gitea"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/log"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/to"
gitea_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
const (
GetRepoRefToolName = "get_repo_ref"
ListAllGitRefsToolName = "list_all_git_refs"
GetTreeToolName = "get_tree"
GetRepoNoteToolName = "get_repo_note"
CompareCommitsToolName = "compare_commits"
)
var (
GetRepoRefTool = mcp.NewTool(
GetRepoRefToolName,
mcp.WithDescription("Get a git reference from a repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("ref", mcp.Required(), mcp.Description("git reference (e.g., refs/heads/main, refs/tags/v1.0)")),
)
ListAllGitRefsTool = mcp.NewTool(
ListAllGitRefsToolName,
mcp.WithDescription("List all git references in a repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
)
GetTreeTool = mcp.NewTool(
GetTreeToolName,
mcp.WithDescription("Get the tree of a repository at a given SHA"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("sha", mcp.Required(), mcp.Description("SHA of the tree")),
mcp.WithBoolean("recursive", mcp.Description("show all items recursively")),
)
GetRepoNoteTool = mcp.NewTool(
GetRepoNoteToolName,
mcp.WithDescription("Get a git note for a commit"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("sha", mcp.Required(), mcp.Description("commit SHA")),
)
CompareCommitsTool = mcp.NewTool(
CompareCommitsToolName,
mcp.WithDescription("Compare two commits in a repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("base", mcp.Required(), mcp.Description("base commit SHA or branch")),
mcp.WithString("head", mcp.Required(), mcp.Description("head commit SHA or branch")),
)
)
func init() {
Tool.RegisterRead(server.ServerTool{Tool: GetRepoRefTool, Handler: GetRepoRefFn})
Tool.RegisterRead(server.ServerTool{Tool: ListAllGitRefsTool, Handler: ListAllGitRefsFn})
Tool.RegisterRead(server.ServerTool{Tool: GetTreeTool, Handler: GetTreeFn})
Tool.RegisterRead(server.ServerTool{Tool: GetRepoNoteTool, Handler: GetRepoNoteFn})
Tool.RegisterRead(server.ServerTool{Tool: CompareCommitsTool, Handler: CompareCommitsFn})
}
func GetRepoRefFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetRepoRefFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
ref, _ := req.GetArguments()["ref"].(string)
if owner == "" || repo == "" || ref == "" {
return to.ErrorResult(errors.New("owner, repo, and ref are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
reference, _, err := client.GetRepoRef(owner, repo, ref)
if err != nil {
return to.ErrorResult(fmt.Errorf("get repo ref err: %v", err))
}
return to.TextResult(reference)
}
func ListAllGitRefsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListAllGitRefsFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
refs, _, err := client.ListAllGitRefs(owner, repo)
if err != nil {
return to.ErrorResult(fmt.Errorf("list git refs err: %v", err))
}
return to.TextResult(refs)
}
func GetTreeFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetTreeFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
sha, _ := req.GetArguments()["sha"].(string)
if owner == "" || repo == "" || sha == "" {
return to.ErrorResult(errors.New("owner, repo, and sha are required"))
}
opt := gitea_sdk.ListTreeOptions{
Ref: sha,
}
if v, ok := req.GetArguments()["recursive"].(bool); ok && v {
opt.Recursive = true
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
tree, _, err := client.GetTrees(owner, repo, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("get tree err: %v", err))
}
return to.TextResult(tree)
}
func GetRepoNoteFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetRepoNoteFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
sha, _ := req.GetArguments()["sha"].(string)
if owner == "" || repo == "" || sha == "" {
return to.ErrorResult(errors.New("owner, repo, and sha are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
note, _, err := client.GetRepoNote(owner, repo, sha, gitea_sdk.GetRepoNoteOptions{})
if err != nil {
return to.ErrorResult(fmt.Errorf("get repo note err: %v", err))
}
return to.TextResult(note)
}
func CompareCommitsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CompareCommitsFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
base, _ := req.GetArguments()["base"].(string)
head, _ := req.GetArguments()["head"].(string)
if owner == "" || repo == "" || base == "" || head == "" {
return to.ErrorResult(errors.New("owner, repo, base, and head are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
compare, _, err := client.CompareCommits(owner, repo, base, head)
if err != nil {
return to.ErrorResult(fmt.Errorf("compare commits err: %v", err))
}
return to.TextResult(compare)
}

227
operation/repo/hook.go Normal file
View File

@@ -0,0 +1,227 @@
package repo
import (
"context"
"errors"
"fmt"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/gitea"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/log"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/params"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/to"
gitea_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
const (
ListRepoHooksToolName = "list_repo_hooks"
GetRepoHookToolName = "get_repo_hook"
CreateRepoHookToolName = "create_repo_hook"
EditRepoHookToolName = "edit_repo_hook"
DeleteRepoHookToolName = "delete_repo_hook"
)
var (
ListRepoHooksTool = mcp.NewTool(
ListRepoHooksToolName,
mcp.WithDescription("List a repository's webhooks"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)),
)
GetRepoHookTool = mcp.NewTool(
GetRepoHookToolName,
mcp.WithDescription("Get a repository webhook by ID"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("id", mcp.Required(), mcp.Description("webhook ID")),
)
CreateRepoHookTool = mcp.NewTool(
CreateRepoHookToolName,
mcp.WithDescription("Create a webhook for a repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("type", mcp.Required(), mcp.Description("hook type: gitea, gogs, slack, discord, dingtalk, telegram, msteams, feishu, wechatwork, packagist")),
mcp.WithString("url", mcp.Required(), mcp.Description("target URL for the webhook")),
mcp.WithString("content_type", mcp.Description("content type: json or form (default: json)")),
mcp.WithString("secret", mcp.Description("webhook secret")),
mcp.WithBoolean("active", mcp.Description("whether the webhook is active (default: true)")),
)
EditRepoHookTool = mcp.NewTool(
EditRepoHookToolName,
mcp.WithDescription("Edit a repository webhook"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("id", mcp.Required(), mcp.Description("webhook ID")),
mcp.WithString("url", mcp.Description("target URL")),
mcp.WithString("content_type", mcp.Description("content type: json or form")),
mcp.WithString("secret", mcp.Description("webhook secret")),
mcp.WithBoolean("active", mcp.Description("whether the webhook is active")),
)
DeleteRepoHookTool = mcp.NewTool(
DeleteRepoHookToolName,
mcp.WithDescription("Delete a repository webhook"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("id", mcp.Required(), mcp.Description("webhook ID")),
)
)
func init() {
Tool.RegisterRead(server.ServerTool{Tool: ListRepoHooksTool, Handler: ListRepoHooksFn})
Tool.RegisterRead(server.ServerTool{Tool: GetRepoHookTool, Handler: GetRepoHookFn})
Tool.RegisterWrite(server.ServerTool{Tool: CreateRepoHookTool, Handler: CreateRepoHookFn})
Tool.RegisterWrite(server.ServerTool{Tool: EditRepoHookTool, Handler: EditRepoHookFn})
Tool.RegisterWrite(server.ServerTool{Tool: DeleteRepoHookTool, Handler: DeleteRepoHookFn})
}
func ListRepoHooksFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListRepoHooksFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
hooks, _, err := client.ListRepoHooks(owner, repo, gitea_sdk.ListHooksOptions{
ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)},
})
if err != nil {
return to.ErrorResult(fmt.Errorf("list repo hooks err: %v", err))
}
return to.TextResult(hooks)
}
func GetRepoHookFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetRepoHookFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
id, err := params.GetIndex(req.GetArguments(), "id")
if err != nil {
return to.ErrorResult(err)
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
hook, _, err := client.GetRepoHook(owner, repo, id)
if err != nil {
return to.ErrorResult(fmt.Errorf("get repo hook err: %v", err))
}
return to.TextResult(hook)
}
func CreateRepoHookFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CreateRepoHookFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
hookType, _ := req.GetArguments()["type"].(string)
url, _ := req.GetArguments()["url"].(string)
if owner == "" || repo == "" || hookType == "" || url == "" {
return to.ErrorResult(errors.New("owner, repo, type, and url are required"))
}
contentType := "json"
if v, ok := req.GetArguments()["content_type"].(string); ok {
contentType = v
}
config := map[string]string{
"url": url,
"content_type": contentType,
}
if v, ok := req.GetArguments()["secret"].(string); ok {
config["secret"] = v
}
opt := gitea_sdk.CreateHookOption{
Type: gitea_sdk.HookType(hookType),
Config: config,
Active: true,
}
if v, ok := req.GetArguments()["active"].(bool); ok {
opt.Active = v
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
hook, _, err := client.CreateRepoHook(owner, repo, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("create repo hook err: %v", err))
}
return to.TextResult(hook)
}
func EditRepoHookFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called EditRepoHookFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
id, err := params.GetIndex(req.GetArguments(), "id")
if err != nil {
return to.ErrorResult(err)
}
opt := gitea_sdk.EditHookOption{}
config := map[string]string{}
if v, ok := req.GetArguments()["url"].(string); ok {
config["url"] = v
}
if v, ok := req.GetArguments()["content_type"].(string); ok {
config["content_type"] = v
}
if v, ok := req.GetArguments()["secret"].(string); ok {
config["secret"] = v
}
if len(config) > 0 {
opt.Config = config
}
if v, ok := req.GetArguments()["active"].(bool); ok {
opt.Active = &v
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.EditRepoHook(owner, repo, id, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("edit repo hook err: %v", err))
}
return to.TextResult(map[string]string{"status": "updated"})
}
func DeleteRepoHookFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called DeleteRepoHookFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
id, err := params.GetIndex(req.GetArguments(), "id")
if err != nil {
return to.ErrorResult(err)
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.DeleteRepoHook(owner, repo, id)
if err != nil {
return to.ErrorResult(fmt.Errorf("delete repo hook err: %v", err))
}
return to.TextResult(map[string]string{"status": "deleted"})
}

156
operation/repo/mirror.go Normal file
View File

@@ -0,0 +1,156 @@
package repo
import (
"context"
"errors"
"fmt"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/gitea"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/log"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/to"
gitea_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
const (
CreatePushMirrorToolName = "create_push_mirror"
ListPushMirrorsToolName = "list_push_mirrors"
GetPushMirrorByRemoteNameToolName = "get_push_mirror"
DeletePushMirrorToolName = "delete_push_mirror"
)
var (
CreatePushMirrorTool = mcp.NewTool(
CreatePushMirrorToolName,
mcp.WithDescription("Create a push mirror for a repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("remote_address", mcp.Required(), mcp.Description("remote git URL to push to")),
mcp.WithString("remote_username", mcp.Description("remote username for authentication")),
mcp.WithString("remote_password", mcp.Description("remote password/token for authentication")),
mcp.WithString("interval", mcp.Description("sync interval (e.g., 8h0m0s)")),
mcp.WithBoolean("sync_on_commit", mcp.Description("sync on commit")),
)
ListPushMirrorsTool = mcp.NewTool(
ListPushMirrorsToolName,
mcp.WithDescription("List push mirrors for a repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
)
GetPushMirrorByRemoteNameTool = mcp.NewTool(
GetPushMirrorByRemoteNameToolName,
mcp.WithDescription("Get a push mirror by remote name"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("remote_name", mcp.Required(), mcp.Description("remote name")),
)
DeletePushMirrorTool = mcp.NewTool(
DeletePushMirrorToolName,
mcp.WithDescription("Delete a push mirror by remote name"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("remote_name", mcp.Required(), mcp.Description("remote name to delete")),
)
)
func init() {
Tool.RegisterRead(server.ServerTool{Tool: ListPushMirrorsTool, Handler: ListPushMirrorsFn})
Tool.RegisterRead(server.ServerTool{Tool: GetPushMirrorByRemoteNameTool, Handler: GetPushMirrorByRemoteNameFn})
Tool.RegisterWrite(server.ServerTool{Tool: CreatePushMirrorTool, Handler: CreatePushMirrorFn})
Tool.RegisterWrite(server.ServerTool{Tool: DeletePushMirrorTool, Handler: DeletePushMirrorFn})
}
func CreatePushMirrorFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CreatePushMirrorFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
remoteAddr, _ := req.GetArguments()["remote_address"].(string)
if owner == "" || repo == "" || remoteAddr == "" {
return to.ErrorResult(errors.New("owner, repo, and remote_address are required"))
}
opt := gitea_sdk.CreatePushMirrorOption{
RemoteAddress: remoteAddr,
}
if v, ok := req.GetArguments()["remote_username"].(string); ok {
opt.RemoteUsername = v
}
if v, ok := req.GetArguments()["remote_password"].(string); ok {
opt.RemotePassword = v
}
if v, ok := req.GetArguments()["interval"].(string); ok {
opt.Interval = v
}
if v, ok := req.GetArguments()["sync_on_commit"].(bool); ok {
opt.SyncONCommit = v
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
mirror, _, err := client.PushMirrors(owner, repo, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("create push mirror err: %v", err))
}
return to.TextResult(mirror)
}
func ListPushMirrorsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListPushMirrorsFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
mirrors, _, err := client.ListPushMirrors(owner, repo, gitea_sdk.ListOptions{})
if err != nil {
return to.ErrorResult(fmt.Errorf("list push mirrors err: %v", err))
}
return to.TextResult(mirrors)
}
func GetPushMirrorByRemoteNameFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetPushMirrorByRemoteNameFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
remoteName, _ := req.GetArguments()["remote_name"].(string)
if owner == "" || repo == "" || remoteName == "" {
return to.ErrorResult(errors.New("owner, repo, and remote_name are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
mirror, _, err := client.GetPushMirrorByRemoteName(owner, repo, remoteName)
if err != nil {
return to.ErrorResult(fmt.Errorf("get push mirror err: %v", err))
}
return to.TextResult(mirror)
}
func DeletePushMirrorFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called DeletePushMirrorFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
remoteName, _ := req.GetArguments()["remote_name"].(string)
if owner == "" || repo == "" || remoteName == "" {
return to.ErrorResult(errors.New("owner, repo, and remote_name are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.DeletePushMirror(owner, repo, remoteName)
if err != nil {
return to.ErrorResult(fmt.Errorf("delete push mirror err: %v", err))
}
return to.TextResult(map[string]string{"status": "deleted"})
}

View File

@@ -2,13 +2,14 @@ package repo
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"time" "time"
"gitea.com/gitea/gitea-mcp/pkg/gitea" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/gitea"
"gitea.com/gitea/gitea-mcp/pkg/log" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/log"
"gitea.com/gitea/gitea-mcp/pkg/ptr" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/params"
"gitea.com/gitea/gitea-mcp/pkg/to" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/to"
gitea_sdk "code.gitea.io/sdk/gitea" gitea_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/mcp"
@@ -112,23 +113,23 @@ func CreateReleaseFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToo
log.Debugf("Called CreateReleasesFn") log.Debugf("Called CreateReleasesFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return nil, fmt.Errorf("owner is required") return nil, errors.New("owner is required")
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return nil, fmt.Errorf("repo is required") return nil, errors.New("repo is required")
} }
tagName, ok := req.GetArguments()["tag_name"].(string) tagName, ok := req.GetArguments()["tag_name"].(string)
if !ok { if !ok {
return nil, fmt.Errorf("tag_name is required") return nil, errors.New("tag_name is required")
} }
target, ok := req.GetArguments()["target"].(string) target, ok := req.GetArguments()["target"].(string)
if !ok { if !ok {
return nil, fmt.Errorf("target is required") return nil, errors.New("target is required")
} }
title, ok := req.GetArguments()["title"].(string) title, ok := req.GetArguments()["title"].(string)
if !ok { if !ok {
return nil, fmt.Errorf("title is required") return nil, errors.New("title is required")
} }
isDraft, _ := req.GetArguments()["is_draft"].(bool) isDraft, _ := req.GetArguments()["is_draft"].(bool)
isPreRelease, _ := req.GetArguments()["is_pre_release"].(bool) isPreRelease, _ := req.GetArguments()["is_pre_release"].(bool)
@@ -157,22 +158,22 @@ func DeleteReleaseFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToo
log.Debugf("Called DeleteReleaseFn") log.Debugf("Called DeleteReleaseFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return nil, fmt.Errorf("owner is required") return nil, errors.New("owner is required")
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return nil, fmt.Errorf("repo is required") return nil, errors.New("repo is required")
} }
id, ok := req.GetArguments()["id"].(float64) id, err := params.GetIndex(req.GetArguments(), "id")
if !ok { if err != nil {
return nil, fmt.Errorf("id is required") return to.ErrorResult(err)
} }
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
} }
_, err = client.DeleteRelease(owner, repo, int64(id)) _, err = client.DeleteRelease(owner, repo, id)
if err != nil { if err != nil {
return nil, fmt.Errorf("delete release error: %v", err) return nil, fmt.Errorf("delete release error: %v", err)
} }
@@ -184,22 +185,22 @@ func GetReleaseFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRe
log.Debugf("Called GetReleaseFn") log.Debugf("Called GetReleaseFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return nil, fmt.Errorf("owner is required") return nil, errors.New("owner is required")
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return nil, fmt.Errorf("repo is required") return nil, errors.New("repo is required")
} }
id, ok := req.GetArguments()["id"].(float64) id, err := params.GetIndex(req.GetArguments(), "id")
if !ok { if err != nil {
return nil, fmt.Errorf("id is required") return to.ErrorResult(err)
} }
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
} }
release, _, err := client.GetRelease(owner, repo, int64(id)) release, _, err := client.GetRelease(owner, repo, id)
if err != nil { if err != nil {
return nil, fmt.Errorf("get release error: %v", err) return nil, fmt.Errorf("get release error: %v", err)
} }
@@ -211,11 +212,11 @@ func GetLatestReleaseFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call
log.Debugf("Called GetLatestReleaseFn") log.Debugf("Called GetLatestReleaseFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return nil, fmt.Errorf("owner is required") return nil, errors.New("owner is required")
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return nil, fmt.Errorf("repo is required") return nil, errors.New("repo is required")
} }
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
@@ -234,24 +235,24 @@ func ListReleasesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTool
log.Debugf("Called ListReleasesFn") log.Debugf("Called ListReleasesFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return nil, fmt.Errorf("owner is required") return nil, errors.New("owner is required")
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return nil, fmt.Errorf("repo is required") return nil, errors.New("repo is required")
} }
var pIsDraft *bool var pIsDraft *bool
isDraft, ok := req.GetArguments()["is_draft"].(bool) isDraft, ok := req.GetArguments()["is_draft"].(bool)
if ok { if ok {
pIsDraft = ptr.To(isDraft) pIsDraft = new(isDraft)
} }
var pIsPreRelease *bool var pIsPreRelease *bool
isPreRelease, ok := req.GetArguments()["is_pre_release"].(bool) isPreRelease, ok := req.GetArguments()["is_pre_release"].(bool)
if ok { if ok {
pIsPreRelease = ptr.To(isPreRelease) pIsPreRelease = new(isPreRelease)
} }
page, _ := req.GetArguments()["page"].(float64) page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize, _ := req.GetArguments()["pageSize"].(float64) pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 20)
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
if err != nil { if err != nil {

View File

@@ -5,11 +5,11 @@ import (
"errors" "errors"
"fmt" "fmt"
"gitea.com/gitea/gitea-mcp/pkg/gitea" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/gitea"
"gitea.com/gitea/gitea-mcp/pkg/log" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/log"
"gitea.com/gitea/gitea-mcp/pkg/ptr" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/params"
"gitea.com/gitea/gitea-mcp/pkg/to" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/to"
"gitea.com/gitea/gitea-mcp/pkg/tool" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/tool"
gitea_sdk "code.gitea.io/sdk/gitea" gitea_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/mcp"
@@ -166,12 +166,12 @@ func ForkRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResu
return to.ErrorResult(errors.New("repository name is required")) return to.ErrorResult(errors.New("repository name is required"))
} }
organization, ok := req.GetArguments()["organization"].(string) organization, ok := req.GetArguments()["organization"].(string)
organizationPtr := ptr.To(organization) organizationPtr := new(organization)
if !ok || organization == "" { if !ok || organization == "" {
organizationPtr = nil organizationPtr = nil
} }
name, ok := req.GetArguments()["name"].(string) name, ok := req.GetArguments()["name"].(string)
namePtr := ptr.To(name) namePtr := new(name)
if !ok || name == "" { if !ok || name == "" {
namePtr = nil namePtr = nil
} }
@@ -192,14 +192,8 @@ func ForkRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResu
func ListMyReposFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func ListMyReposFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListMyReposFn") log.Debugf("Called ListMyReposFn")
page, ok := req.GetArguments()["page"].(float64) page := params.GetOptionalInt(req.GetArguments(), "page", 1)
if !ok { pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
page = 1
}
pageSize, ok := req.GetArguments()["pageSize"].(float64)
if !ok {
pageSize = 100
}
opt := gitea_sdk.ListReposOptions{ opt := gitea_sdk.ListReposOptions{
ListOptions: gitea_sdk.ListOptions{ ListOptions: gitea_sdk.ListOptions{
Page: int(page), Page: int(page),

View File

@@ -0,0 +1,216 @@
package repo
import (
"context"
"errors"
"fmt"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/gitea"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/log"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/params"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/to"
gitea_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
const (
ListStargazersToolName = "list_stargazers"
ListForksToolName = "list_forks"
GetWatchedReposToolName = "get_watched_repos"
GetMyWatchedReposToolName = "get_my_watched_repos"
CheckRepoWatchToolName = "check_repo_watch"
WatchRepoToolName = "watch_repo"
UnWatchRepoToolName = "unwatch_repo"
)
var (
ListStargazersTool = mcp.NewTool(
ListStargazersToolName,
mcp.WithDescription("List a repository's stargazers"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)),
)
ListForksTool = mcp.NewTool(
ListForksToolName,
mcp.WithDescription("List a repository's forks"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)),
)
GetWatchedReposTool = mcp.NewTool(
GetWatchedReposToolName,
mcp.WithDescription("List repositories watched by a user"),
mcp.WithString("username", mcp.Required(), mcp.Description("username")),
)
GetMyWatchedReposTool = mcp.NewTool(
GetMyWatchedReposToolName,
mcp.WithDescription("List repositories watched by the authenticated user"),
)
CheckRepoWatchTool = mcp.NewTool(
CheckRepoWatchToolName,
mcp.WithDescription("Check if the authenticated user is watching a repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
)
WatchRepoTool = mcp.NewTool(
WatchRepoToolName,
mcp.WithDescription("Watch a repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
)
UnWatchRepoTool = mcp.NewTool(
UnWatchRepoToolName,
mcp.WithDescription("Unwatch a repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
)
)
func init() {
Tool.RegisterRead(server.ServerTool{Tool: ListStargazersTool, Handler: ListStargazersFn})
Tool.RegisterRead(server.ServerTool{Tool: ListForksTool, Handler: ListForksFn})
Tool.RegisterRead(server.ServerTool{Tool: GetWatchedReposTool, Handler: GetWatchedReposFn})
Tool.RegisterRead(server.ServerTool{Tool: GetMyWatchedReposTool, Handler: GetMyWatchedReposFn})
Tool.RegisterRead(server.ServerTool{Tool: CheckRepoWatchTool, Handler: CheckRepoWatchFn})
Tool.RegisterWrite(server.ServerTool{Tool: WatchRepoTool, Handler: WatchRepoFn})
Tool.RegisterWrite(server.ServerTool{Tool: UnWatchRepoTool, Handler: UnWatchRepoFn})
}
func ListStargazersFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListStargazersFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
users, _, err := client.ListRepoStargazers(owner, repo, gitea_sdk.ListStargazersOptions{
ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)},
})
if err != nil {
return to.ErrorResult(fmt.Errorf("list stargazers err: %v", err))
}
return to.TextResult(users)
}
func ListForksFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListForksFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
repos, _, err := client.ListForks(owner, repo, gitea_sdk.ListForksOptions{
ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)},
})
if err != nil {
return to.ErrorResult(fmt.Errorf("list forks err: %v", err))
}
return to.TextResult(repos)
}
func GetWatchedReposFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetWatchedReposFn")
username, _ := req.GetArguments()["username"].(string)
if username == "" {
return to.ErrorResult(errors.New("username is required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
repos, _, err := client.GetWatchedRepos(username)
if err != nil {
return to.ErrorResult(fmt.Errorf("get watched repos err: %v", err))
}
return to.TextResult(repos)
}
func GetMyWatchedReposFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetMyWatchedReposFn")
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
repos, _, err := client.GetMyWatchedRepos()
if err != nil {
return to.ErrorResult(fmt.Errorf("get my watched repos err: %v", err))
}
return to.TextResult(repos)
}
func CheckRepoWatchFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CheckRepoWatchFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
watching, _, err := client.CheckRepoWatch(owner, repo)
if err != nil {
return to.ErrorResult(fmt.Errorf("check repo watch err: %v", err))
}
return to.TextResult(map[string]any{"watching": watching})
}
func WatchRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called WatchRepoFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.WatchRepo(owner, repo)
if err != nil {
return to.ErrorResult(fmt.Errorf("watch repo err: %v", err))
}
return to.TextResult(map[string]string{"status": "watching"})
}
func UnWatchRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called UnWatchRepoFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.UnWatchRepo(owner, repo)
if err != nil {
return to.ErrorResult(fmt.Errorf("unwatch repo err: %v", err))
}
return to.TextResult(map[string]string{"status": "unwatched"})
}

134
operation/repo/status.go Normal file
View File

@@ -0,0 +1,134 @@
package repo
import (
"context"
"errors"
"fmt"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/gitea"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/log"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/params"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/to"
gitea_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
const (
CreateStatusToolName = "create_commit_status"
ListStatusesToolName = "list_commit_statuses"
GetCombinedStatusToolName = "get_combined_status"
)
var (
CreateStatusTool = mcp.NewTool(
CreateStatusToolName,
mcp.WithDescription("Create a commit status"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("sha", mcp.Required(), mcp.Description("commit SHA")),
mcp.WithString("state", mcp.Required(), mcp.Description("status state: pending, success, error, failure, warning")),
mcp.WithString("target_url", mcp.Description("URL for status details")),
mcp.WithString("description", mcp.Description("status description")),
mcp.WithString("context", mcp.Description("status context (e.g., ci/build)")),
)
ListStatusesTool = mcp.NewTool(
ListStatusesToolName,
mcp.WithDescription("List commit statuses for a ref"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("ref", mcp.Required(), mcp.Description("commit SHA, branch, or tag")),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)),
)
GetCombinedStatusTool = mcp.NewTool(
GetCombinedStatusToolName,
mcp.WithDescription("Get the combined status for a ref"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("ref", mcp.Required(), mcp.Description("commit SHA, branch, or tag")),
)
)
func init() {
Tool.RegisterRead(server.ServerTool{Tool: ListStatusesTool, Handler: ListStatusesFn})
Tool.RegisterRead(server.ServerTool{Tool: GetCombinedStatusTool, Handler: GetCombinedStatusFn})
Tool.RegisterWrite(server.ServerTool{Tool: CreateStatusTool, Handler: CreateStatusFn})
}
func CreateStatusFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CreateStatusFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
sha, _ := req.GetArguments()["sha"].(string)
state, _ := req.GetArguments()["state"].(string)
if owner == "" || repo == "" || sha == "" || state == "" {
return to.ErrorResult(errors.New("owner, repo, sha, and state are required"))
}
opt := gitea_sdk.CreateStatusOption{
State: gitea_sdk.StatusState(state),
}
if v, ok := req.GetArguments()["target_url"].(string); ok {
opt.TargetURL = v
}
if v, ok := req.GetArguments()["description"].(string); ok {
opt.Description = v
}
if v, ok := req.GetArguments()["context"].(string); ok {
opt.Context = v
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
status, _, err := client.CreateStatus(owner, repo, sha, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("create status err: %v", err))
}
return to.TextResult(status)
}
func ListStatusesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListStatusesFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
ref, _ := req.GetArguments()["ref"].(string)
if owner == "" || repo == "" || ref == "" {
return to.ErrorResult(errors.New("owner, repo, and ref are required"))
}
page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
statuses, _, err := client.ListStatuses(owner, repo, ref, gitea_sdk.ListStatusesOption{
ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)},
})
if err != nil {
return to.ErrorResult(fmt.Errorf("list statuses err: %v", err))
}
return to.TextResult(statuses)
}
func GetCombinedStatusFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetCombinedStatusFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
ref, _ := req.GetArguments()["ref"].(string)
if owner == "" || repo == "" || ref == "" {
return to.ErrorResult(errors.New("owner, repo, and ref are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
combined, _, err := client.GetCombinedStatus(owner, repo, ref)
if err != nil {
return to.ErrorResult(fmt.Errorf("get combined status err: %v", err))
}
return to.TextResult(combined)
}

View File

@@ -2,11 +2,13 @@ package repo
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"gitea.com/gitea/gitea-mcp/pkg/gitea" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/gitea"
"gitea.com/gitea/gitea-mcp/pkg/log" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/log"
"gitea.com/gitea/gitea-mcp/pkg/to" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/params"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/to"
gitea_sdk "code.gitea.io/sdk/gitea" gitea_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/mcp"
@@ -89,15 +91,15 @@ func CreateTagFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRes
log.Debugf("Called CreateTagFn") log.Debugf("Called CreateTagFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return nil, fmt.Errorf("owner is required") return nil, errors.New("owner is required")
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return nil, fmt.Errorf("repo is required") return nil, errors.New("repo is required")
} }
tagName, ok := req.GetArguments()["tag_name"].(string) tagName, ok := req.GetArguments()["tag_name"].(string)
if !ok { if !ok {
return nil, fmt.Errorf("tag_name is required") return nil, errors.New("tag_name is required")
} }
target, _ := req.GetArguments()["target"].(string) target, _ := req.GetArguments()["target"].(string)
message, _ := req.GetArguments()["message"].(string) message, _ := req.GetArguments()["message"].(string)
@@ -122,15 +124,15 @@ func DeleteTagFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRes
log.Debugf("Called DeleteTagFn") log.Debugf("Called DeleteTagFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return nil, fmt.Errorf("owner is required") return nil, errors.New("owner is required")
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return nil, fmt.Errorf("repo is required") return nil, errors.New("repo is required")
} }
tagName, ok := req.GetArguments()["tag_name"].(string) tagName, ok := req.GetArguments()["tag_name"].(string)
if !ok { if !ok {
return nil, fmt.Errorf("tag_name is required") return nil, errors.New("tag_name is required")
} }
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
@@ -149,15 +151,15 @@ func GetTagFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult
log.Debugf("Called GetTagFn") log.Debugf("Called GetTagFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return nil, fmt.Errorf("owner is required") return nil, errors.New("owner is required")
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return nil, fmt.Errorf("repo is required") return nil, errors.New("repo is required")
} }
tagName, ok := req.GetArguments()["tag_name"].(string) tagName, ok := req.GetArguments()["tag_name"].(string)
if !ok { if !ok {
return nil, fmt.Errorf("tag_name is required") return nil, errors.New("tag_name is required")
} }
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
@@ -176,14 +178,14 @@ func ListTagsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResu
log.Debugf("Called ListTagsFn") log.Debugf("Called ListTagsFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return nil, fmt.Errorf("owner is required") return nil, errors.New("owner is required")
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return nil, fmt.Errorf("repo is required") return nil, errors.New("repo is required")
} }
page, _ := req.GetArguments()["page"].(float64) page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize, _ := req.GetArguments()["pageSize"].(float64) pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 20)
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
if err != nil { if err != nil {

View File

@@ -0,0 +1,200 @@
package repo
import (
"context"
"errors"
"fmt"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/gitea"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/log"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/params"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/to"
gitea_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
const (
ListTagProtectionsToolName = "list_tag_protections"
GetTagProtectionToolName = "get_tag_protection"
CreateTagProtectionToolName = "create_tag_protection"
EditTagProtectionToolName = "edit_tag_protection"
DeleteTagProtectionToolName = "delete_tag_protection"
)
var (
ListTagProtectionsTool = mcp.NewTool(
ListTagProtectionsToolName,
mcp.WithDescription("List tag protections for a repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
)
GetTagProtectionTool = mcp.NewTool(
GetTagProtectionToolName,
mcp.WithDescription("Get a tag protection rule by ID"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("id", mcp.Required(), mcp.Description("tag protection ID")),
)
CreateTagProtectionTool = mcp.NewTool(
CreateTagProtectionToolName,
mcp.WithDescription("Create a tag protection rule"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("name_pattern", mcp.Required(), mcp.Description("glob pattern for tag name matching")),
mcp.WithString("whitelist_usernames", mcp.Description("comma-separated list of allowed usernames")),
mcp.WithString("whitelist_teams", mcp.Description("comma-separated list of allowed team names")),
)
EditTagProtectionTool = mcp.NewTool(
EditTagProtectionToolName,
mcp.WithDescription("Edit a tag protection rule"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("id", mcp.Required(), mcp.Description("tag protection ID")),
mcp.WithString("name_pattern", mcp.Description("glob pattern for tag name matching")),
mcp.WithString("whitelist_usernames", mcp.Description("comma-separated list of allowed usernames")),
mcp.WithString("whitelist_teams", mcp.Description("comma-separated list of allowed team names")),
)
DeleteTagProtectionTool = mcp.NewTool(
DeleteTagProtectionToolName,
mcp.WithDescription("Delete a tag protection rule"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("id", mcp.Required(), mcp.Description("tag protection ID")),
)
)
func init() {
Tool.RegisterRead(server.ServerTool{Tool: ListTagProtectionsTool, Handler: ListTagProtectionsFn})
Tool.RegisterRead(server.ServerTool{Tool: GetTagProtectionTool, Handler: GetTagProtectionFn})
Tool.RegisterWrite(server.ServerTool{Tool: CreateTagProtectionTool, Handler: CreateTagProtectionFn})
Tool.RegisterWrite(server.ServerTool{Tool: EditTagProtectionTool, Handler: EditTagProtectionFn})
Tool.RegisterWrite(server.ServerTool{Tool: DeleteTagProtectionTool, Handler: DeleteTagProtectionFn})
}
func ListTagProtectionsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListTagProtectionsFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
tps, _, err := client.ListTagProtection(owner, repo, gitea_sdk.ListRepoTagProtectionsOptions{})
if err != nil {
return to.ErrorResult(fmt.Errorf("list tag protections err: %v", err))
}
return to.TextResult(tps)
}
func GetTagProtectionFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetTagProtectionFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
id, err := params.GetIndex(req.GetArguments(), "id")
if err != nil {
return to.ErrorResult(err)
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
tp, _, err := client.GetTagProtection(owner, repo, id)
if err != nil {
return to.ErrorResult(fmt.Errorf("get tag protection err: %v", err))
}
return to.TextResult(tp)
}
func CreateTagProtectionFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CreateTagProtectionFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
namePattern, _ := req.GetArguments()["name_pattern"].(string)
if owner == "" || repo == "" || namePattern == "" {
return to.ErrorResult(errors.New("owner, repo, and name_pattern are required"))
}
opt := gitea_sdk.CreateTagProtectionOption{
NamePattern: namePattern,
}
if v, ok := req.GetArguments()["whitelist_usernames"].(string); ok && v != "" {
opt.WhitelistUsernames = splitAndTrim(v)
}
if v, ok := req.GetArguments()["whitelist_teams"].(string); ok && v != "" {
opt.WhitelistTeams = splitAndTrim(v)
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
tp, _, err := client.CreateTagProtection(owner, repo, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("create tag protection err: %v", err))
}
return to.TextResult(tp)
}
func EditTagProtectionFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called EditTagProtectionFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
id, err := params.GetIndex(req.GetArguments(), "id")
if err != nil {
return to.ErrorResult(err)
}
opt := gitea_sdk.EditTagProtectionOption{}
if v, ok := req.GetArguments()["name_pattern"].(string); ok {
opt.NamePattern = &v
}
if v, ok := req.GetArguments()["whitelist_usernames"].(string); ok {
opt.WhitelistUsernames = splitAndTrim(v)
}
if v, ok := req.GetArguments()["whitelist_teams"].(string); ok {
opt.WhitelistTeams = splitAndTrim(v)
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
tp, _, err := client.EditTagProtection(owner, repo, id, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("edit tag protection err: %v", err))
}
return to.TextResult(tp)
}
func DeleteTagProtectionFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called DeleteTagProtectionFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
id, err := params.GetIndex(req.GetArguments(), "id")
if err != nil {
return to.ErrorResult(err)
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.DeleteTagProtection(owner, repo, id)
if err != nil {
return to.ErrorResult(fmt.Errorf("delete tag protection err: %v", err))
}
return to.TextResult(map[string]string{"status": "deleted"})
}

157
operation/repo/topic.go Normal file
View File

@@ -0,0 +1,157 @@
package repo
import (
"context"
"errors"
"fmt"
"strings"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/gitea"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/log"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/to"
gitea_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
const (
ListRepoTopicsToolName = "list_repo_topics"
SetRepoTopicsToolName = "set_repo_topics"
AddRepoTopicToolName = "add_repo_topic"
DeleteRepoTopicToolName = "delete_repo_topic"
)
var (
ListRepoTopicsTool = mcp.NewTool(
ListRepoTopicsToolName,
mcp.WithDescription("List a repository's topics"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
)
SetRepoTopicsTool = mcp.NewTool(
SetRepoTopicsToolName,
mcp.WithDescription("Replace all topics of a repository with a new list"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("topics", mcp.Required(), mcp.Description("comma-separated list of topics")),
)
AddRepoTopicTool = mcp.NewTool(
AddRepoTopicToolName,
mcp.WithDescription("Add a topic to a repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("topic", mcp.Required(), mcp.Description("topic name to add")),
)
DeleteRepoTopicTool = mcp.NewTool(
DeleteRepoTopicToolName,
mcp.WithDescription("Remove a topic from a repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("topic", mcp.Required(), mcp.Description("topic name to remove")),
)
)
func init() {
Tool.RegisterRead(server.ServerTool{Tool: ListRepoTopicsTool, Handler: ListRepoTopicsFn})
Tool.RegisterWrite(server.ServerTool{Tool: SetRepoTopicsTool, Handler: SetRepoTopicsFn})
Tool.RegisterWrite(server.ServerTool{Tool: AddRepoTopicTool, Handler: AddRepoTopicFn})
Tool.RegisterWrite(server.ServerTool{Tool: DeleteRepoTopicTool, Handler: DeleteRepoTopicFn})
}
func ListRepoTopicsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListRepoTopicsFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
topics, _, err := client.ListRepoTopics(owner, repo, gitea_sdk.ListRepoTopicsOptions{})
if err != nil {
return to.ErrorResult(fmt.Errorf("list repo topics err: %v", err))
}
return to.TextResult(topics)
}
func SetRepoTopicsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called SetRepoTopicsFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
topicsStr, _ := req.GetArguments()["topics"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
var topics []string
if topicsStr != "" {
for _, t := range splitAndTrim(topicsStr) {
if t != "" {
topics = append(topics, t)
}
}
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.SetRepoTopics(owner, repo, topics)
if err != nil {
return to.ErrorResult(fmt.Errorf("set repo topics err: %v", err))
}
return to.TextResult(map[string]any{"status": "updated", "topics": topics})
}
func AddRepoTopicFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called AddRepoTopicFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
topic, _ := req.GetArguments()["topic"].(string)
if owner == "" || repo == "" || topic == "" {
return to.ErrorResult(errors.New("owner, repo, and topic are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.AddRepoTopic(owner, repo, topic)
if err != nil {
return to.ErrorResult(fmt.Errorf("add repo topic err: %v", err))
}
return to.TextResult(map[string]string{"status": "added", "topic": topic})
}
func DeleteRepoTopicFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called DeleteRepoTopicFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
topic, _ := req.GetArguments()["topic"].(string)
if owner == "" || repo == "" || topic == "" {
return to.ErrorResult(errors.New("owner, repo, and topic are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.DeleteRepoTopic(owner, repo, topic)
if err != nil {
return to.ErrorResult(fmt.Errorf("delete repo topic err: %v", err))
}
return to.TextResult(map[string]string{"status": "deleted", "topic": topic})
}
func splitAndTrim(s string) []string {
var result []string
for _, part := range strings.Split(s, ",") {
trimmed := strings.TrimSpace(part)
if trimmed != "" {
result = append(result, trimmed)
}
}
return result
}

167
operation/repo/transfer.go Normal file
View File

@@ -0,0 +1,167 @@
package repo
import (
"context"
"errors"
"fmt"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/gitea"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/log"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/to"
gitea_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
const (
TransferRepoToolName = "transfer_repo"
AcceptRepoTransferToolName = "accept_repo_transfer"
RejectRepoTransferToolName = "reject_repo_transfer"
CreateFromTemplateToolName = "create_repo_from_template"
)
var (
TransferRepoTool = mcp.NewTool(
TransferRepoToolName,
mcp.WithDescription("Transfer a repository to a new owner"),
mcp.WithString("owner", mcp.Required(), mcp.Description("current repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("new_owner", mcp.Required(), mcp.Description("new owner username or org")),
)
AcceptRepoTransferTool = mcp.NewTool(
AcceptRepoTransferToolName,
mcp.WithDescription("Accept a pending repository transfer"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
)
RejectRepoTransferTool = mcp.NewTool(
RejectRepoTransferToolName,
mcp.WithDescription("Reject a pending repository transfer"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
)
CreateFromTemplateTool = mcp.NewTool(
CreateFromTemplateToolName,
mcp.WithDescription("Create a repository from a template"),
mcp.WithString("template_owner", mcp.Required(), mcp.Description("template repository owner")),
mcp.WithString("template_repo", mcp.Required(), mcp.Description("template repository name")),
mcp.WithString("name", mcp.Required(), mcp.Description("name for the new repository")),
mcp.WithString("owner", mcp.Required(), mcp.Description("owner of the new repository (user or org)")),
mcp.WithString("description", mcp.Description("description for the new repository")),
mcp.WithBoolean("private", mcp.Description("whether the new repository is private")),
mcp.WithBoolean("git_content", mcp.Description("copy git content from template (default: true)")),
mcp.WithBoolean("topics", mcp.Description("copy topics from template")),
mcp.WithBoolean("labels", mcp.Description("copy labels from template")),
mcp.WithBoolean("webhooks", mcp.Description("copy webhooks from template")),
)
)
func init() {
Tool.RegisterWrite(server.ServerTool{Tool: TransferRepoTool, Handler: TransferRepoFn})
Tool.RegisterWrite(server.ServerTool{Tool: AcceptRepoTransferTool, Handler: AcceptRepoTransferFn})
Tool.RegisterWrite(server.ServerTool{Tool: RejectRepoTransferTool, Handler: RejectRepoTransferFn})
Tool.RegisterWrite(server.ServerTool{Tool: CreateFromTemplateTool, Handler: CreateFromTemplateFn})
}
func TransferRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called TransferRepoFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
newOwner, _ := req.GetArguments()["new_owner"].(string)
if owner == "" || repo == "" || newOwner == "" {
return to.ErrorResult(errors.New("owner, repo, and new_owner are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
repository, _, err := client.TransferRepo(owner, repo, gitea_sdk.TransferRepoOption{
NewOwner: newOwner,
})
if err != nil {
return to.ErrorResult(fmt.Errorf("transfer repo err: %v", err))
}
return to.TextResult(repository)
}
func AcceptRepoTransferFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called AcceptRepoTransferFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
repository, _, err := client.AcceptRepoTransfer(owner, repo)
if err != nil {
return to.ErrorResult(fmt.Errorf("accept repo transfer err: %v", err))
}
return to.TextResult(repository)
}
func RejectRepoTransferFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called RejectRepoTransferFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
repository, _, err := client.RejectRepoTransfer(owner, repo)
if err != nil {
return to.ErrorResult(fmt.Errorf("reject repo transfer err: %v", err))
}
return to.TextResult(repository)
}
func CreateFromTemplateFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CreateFromTemplateFn")
templateOwner, _ := req.GetArguments()["template_owner"].(string)
templateRepo, _ := req.GetArguments()["template_repo"].(string)
name, _ := req.GetArguments()["name"].(string)
owner, _ := req.GetArguments()["owner"].(string)
if templateOwner == "" || templateRepo == "" || name == "" || owner == "" {
return to.ErrorResult(errors.New("template_owner, template_repo, name, and owner are required"))
}
opt := gitea_sdk.CreateRepoFromTemplateOption{
Name: name,
Owner: owner,
}
if v, ok := req.GetArguments()["description"].(string); ok {
opt.Description = v
}
if v, ok := req.GetArguments()["private"].(bool); ok {
opt.Private = v
}
if v, ok := req.GetArguments()["git_content"].(bool); ok {
opt.GitContent = v
}
if v, ok := req.GetArguments()["topics"].(bool); ok {
opt.Topics = v
}
if v, ok := req.GetArguments()["labels"].(bool); ok {
opt.Labels = v
}
if v, ok := req.GetArguments()["webhooks"].(bool); ok {
opt.Webhooks = v
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
repository, _, err := client.CreateRepoFromTemplate(templateOwner, templateRepo, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("create from template err: %v", err))
}
return to.TextResult(repository)
}

View File

@@ -2,13 +2,14 @@ package search
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"gitea.com/gitea/gitea-mcp/pkg/gitea" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/gitea"
"gitea.com/gitea/gitea-mcp/pkg/log" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/log"
"gitea.com/gitea/gitea-mcp/pkg/ptr" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/params"
"gitea.com/gitea/gitea-mcp/pkg/to" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/to"
"gitea.com/gitea/gitea-mcp/pkg/tool" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/tool"
gitea_sdk "code.gitea.io/sdk/gitea" gitea_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/mcp"
@@ -27,7 +28,7 @@ var (
SearchUsersTool = mcp.NewTool( SearchUsersTool = mcp.NewTool(
SearchUsersToolName, SearchUsersToolName,
mcp.WithDescription("search users"), mcp.WithDescription("search users"),
mcp.WithString("keyword", mcp.Description("Keyword")), mcp.WithString("keyword", mcp.Required(), mcp.Description("Keyword")),
mcp.WithNumber("page", mcp.Description("Page"), mcp.DefaultNumber(1)), mcp.WithNumber("page", mcp.Description("Page"), mcp.DefaultNumber(1)),
mcp.WithNumber("pageSize", mcp.Description("PageSize"), mcp.DefaultNumber(100)), mcp.WithNumber("pageSize", mcp.Description("PageSize"), mcp.DefaultNumber(100)),
) )
@@ -35,8 +36,8 @@ var (
SearOrgTeamsTool = mcp.NewTool( SearOrgTeamsTool = mcp.NewTool(
SearchOrgTeamsToolName, SearchOrgTeamsToolName,
mcp.WithDescription("search organization teams"), mcp.WithDescription("search organization teams"),
mcp.WithString("org", mcp.Description("organization name")), mcp.WithString("org", mcp.Required(), mcp.Description("organization name")),
mcp.WithString("query", mcp.Description("search organization teams")), mcp.WithString("query", mcp.Required(), mcp.Description("search organization teams")),
mcp.WithBoolean("includeDescription", mcp.Description("include description?")), mcp.WithBoolean("includeDescription", mcp.Description("include description?")),
mcp.WithNumber("page", mcp.Description("Page"), mcp.DefaultNumber(1)), mcp.WithNumber("page", mcp.Description("Page"), mcp.DefaultNumber(1)),
mcp.WithNumber("pageSize", mcp.Description("PageSize"), mcp.DefaultNumber(100)), mcp.WithNumber("pageSize", mcp.Description("PageSize"), mcp.DefaultNumber(100)),
@@ -45,7 +46,7 @@ var (
SearchReposTool = mcp.NewTool( SearchReposTool = mcp.NewTool(
SearchReposToolName, SearchReposToolName,
mcp.WithDescription("search repos"), mcp.WithDescription("search repos"),
mcp.WithString("keyword", mcp.Description("Keyword")), mcp.WithString("keyword", mcp.Required(), mcp.Description("Keyword")),
mcp.WithBoolean("keywordIsTopic", mcp.Description("KeywordIsTopic")), mcp.WithBoolean("keywordIsTopic", mcp.Description("KeywordIsTopic")),
mcp.WithBoolean("keywordInDescription", mcp.Description("KeywordInDescription")), mcp.WithBoolean("keywordInDescription", mcp.Description("KeywordInDescription")),
mcp.WithNumber("ownerID", mcp.Description("OwnerID")), mcp.WithNumber("ownerID", mcp.Description("OwnerID")),
@@ -61,32 +62,26 @@ var (
func init() { func init() {
Tool.RegisterRead(server.ServerTool{ Tool.RegisterRead(server.ServerTool{
Tool: SearchUsersTool, Tool: SearchUsersTool,
Handler: SearchUsersFn, Handler: UsersFn,
}) })
Tool.RegisterRead(server.ServerTool{ Tool.RegisterRead(server.ServerTool{
Tool: SearOrgTeamsTool, Tool: SearOrgTeamsTool,
Handler: SearchOrgTeamsFn, Handler: OrgTeamsFn,
}) })
Tool.RegisterRead(server.ServerTool{ Tool.RegisterRead(server.ServerTool{
Tool: SearchReposTool, Tool: SearchReposTool,
Handler: SearchReposFn, Handler: ReposFn,
}) })
} }
func SearchUsersFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func UsersFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called SearchUsersFn") log.Debugf("Called UsersFn")
keyword, ok := req.GetArguments()["keyword"].(string) keyword, ok := req.GetArguments()["keyword"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("keyword is required")) return to.ErrorResult(errors.New("keyword is required"))
}
page, ok := req.GetArguments()["page"].(float64)
if !ok {
page = 1
}
pageSize, ok := req.GetArguments()["pageSize"].(float64)
if !ok {
pageSize = 100
} }
page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
opt := gitea_sdk.SearchUsersOption{ opt := gitea_sdk.SearchUsersOption{
KeyWord: keyword, KeyWord: keyword,
ListOptions: gitea_sdk.ListOptions{ ListOptions: gitea_sdk.ListOptions{
@@ -105,25 +100,19 @@ func SearchUsersFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolR
return to.TextResult(users) return to.TextResult(users)
} }
func SearchOrgTeamsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func OrgTeamsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called SearchOrgTeamsFn") log.Debugf("Called OrgTeamsFn")
org, ok := req.GetArguments()["org"].(string) org, ok := req.GetArguments()["org"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("organization is required")) return to.ErrorResult(errors.New("organization is required"))
} }
query, ok := req.GetArguments()["query"].(string) query, ok := req.GetArguments()["query"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("query is required")) return to.ErrorResult(errors.New("query is required"))
} }
includeDescription, _ := req.GetArguments()["includeDescription"].(bool) includeDescription, _ := req.GetArguments()["includeDescription"].(bool)
page, ok := req.GetArguments()["page"].(float64) page := params.GetOptionalInt(req.GetArguments(), "page", 1)
if !ok { pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
page = 1
}
pageSize, ok := req.GetArguments()["pageSize"].(float64)
if !ok {
pageSize = 100
}
opt := gitea_sdk.SearchTeamsOptions{ opt := gitea_sdk.SearchTeamsOptions{
Query: query, Query: query,
IncludeDescription: includeDescription, IncludeDescription: includeDescription,
@@ -143,40 +132,34 @@ func SearchOrgTeamsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo
return to.TextResult(teams) return to.TextResult(teams)
} }
func SearchReposFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func ReposFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called SearchReposFn") log.Debugf("Called ReposFn")
keyword, ok := req.GetArguments()["keyword"].(string) keyword, ok := req.GetArguments()["keyword"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("keyword is required")) return to.ErrorResult(errors.New("keyword is required"))
} }
keywordIsTopic, _ := req.GetArguments()["keywordIsTopic"].(bool) keywordIsTopic, _ := req.GetArguments()["keywordIsTopic"].(bool)
keywordInDescription, _ := req.GetArguments()["keywordInDescription"].(bool) keywordInDescription, _ := req.GetArguments()["keywordInDescription"].(bool)
ownerID, _ := req.GetArguments()["ownerID"].(float64) ownerID := params.GetOptionalInt(req.GetArguments(), "ownerID", 0)
var pIsPrivate *bool var pIsPrivate *bool
isPrivate, ok := req.GetArguments()["isPrivate"].(bool) isPrivate, ok := req.GetArguments()["isPrivate"].(bool)
if ok { if ok {
pIsPrivate = ptr.To(isPrivate) pIsPrivate = new(isPrivate)
} }
var pIsArchived *bool var pIsArchived *bool
isArchived, ok := req.GetArguments()["isArchived"].(bool) isArchived, ok := req.GetArguments()["isArchived"].(bool)
if ok { if ok {
pIsArchived = ptr.To(isArchived) pIsArchived = new(isArchived)
} }
sort, _ := req.GetArguments()["sort"].(string) sort, _ := req.GetArguments()["sort"].(string)
order, _ := req.GetArguments()["order"].(string) order, _ := req.GetArguments()["order"].(string)
page, ok := req.GetArguments()["page"].(float64) page := params.GetOptionalInt(req.GetArguments(), "page", 1)
if !ok { pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
page = 1
}
pageSize, ok := req.GetArguments()["pageSize"].(float64)
if !ok {
pageSize = 100
}
opt := gitea_sdk.SearchRepoOptions{ opt := gitea_sdk.SearchRepoOptions{
Keyword: keyword, Keyword: keyword,
KeywordIsTopic: keywordIsTopic, KeywordIsTopic: keywordIsTopic,
KeywordInDescription: keywordInDescription, KeywordInDescription: keywordInDescription,
OwnerID: int64(ownerID), OwnerID: ownerID,
IsPrivate: pIsPrivate, IsPrivate: pIsPrivate,
IsArchived: pIsArchived, IsArchived: pIsArchived,
Sort: sort, Sort: sort,

View File

@@ -0,0 +1,42 @@
package search
import (
"slices"
"testing"
"github.com/mark3labs/mcp-go/mcp"
)
func TestSearchToolsRequiredFields(t *testing.T) {
tests := []struct {
name string
tool mcp.Tool
required []string
}{
{
name: "search_users",
tool: SearchUsersTool,
required: []string{"keyword"},
},
{
name: "search_org_teams",
tool: SearOrgTeamsTool,
required: []string{"org", "query"},
},
{
name: "search_repos",
tool: SearchReposTool,
required: []string{"keyword"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
for _, field := range tt.required {
if !slices.Contains(tt.tool.InputSchema.Required, field) {
t.Errorf("tool %s: expected %q to be required, got required=%v", tt.name, field, tt.tool.InputSchema.Required)
}
}
})
}
}

View File

@@ -0,0 +1,104 @@
package settings
import (
"context"
"fmt"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/gitea"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/log"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/to"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/tool"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
var Tool = tool.New()
const (
GetAPISettingsToolName = "get_api_settings"
GetAttachmentSettingsToolName = "get_attachment_settings"
GetRepoSettingsToolName = "get_repo_settings"
GetUISettingsToolName = "get_ui_settings"
)
var (
GetAPISettingsTool = mcp.NewTool(
GetAPISettingsToolName,
mcp.WithDescription("Get the global API settings of the Gitea instance"),
)
GetAttachmentSettingsTool = mcp.NewTool(
GetAttachmentSettingsToolName,
mcp.WithDescription("Get the global attachment settings"),
)
GetRepoSettingsTool = mcp.NewTool(
GetRepoSettingsToolName,
mcp.WithDescription("Get the global repository settings"),
)
GetUISettingsTool = mcp.NewTool(
GetUISettingsToolName,
mcp.WithDescription("Get the global UI settings"),
)
)
func init() {
Tool.RegisterRead(server.ServerTool{Tool: GetAPISettingsTool, Handler: GetAPISettingsFn})
Tool.RegisterRead(server.ServerTool{Tool: GetAttachmentSettingsTool, Handler: GetAttachmentSettingsFn})
Tool.RegisterRead(server.ServerTool{Tool: GetRepoSettingsTool, Handler: GetRepoSettingsFn})
Tool.RegisterRead(server.ServerTool{Tool: GetUISettingsTool, Handler: GetUISettingsFn})
}
func GetAPISettingsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetAPISettingsFn")
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
settings, _, err := client.GetGlobalAPISettings()
if err != nil {
return to.ErrorResult(fmt.Errorf("get API settings err: %v", err))
}
return to.TextResult(settings)
}
func GetAttachmentSettingsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetAttachmentSettingsFn")
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
settings, _, err := client.GetGlobalAttachmentSettings()
if err != nil {
return to.ErrorResult(fmt.Errorf("get attachment settings err: %v", err))
}
return to.TextResult(settings)
}
func GetRepoSettingsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetRepoSettingsFn")
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
settings, _, err := client.GetGlobalRepoSettings()
if err != nil {
return to.ErrorResult(fmt.Errorf("get repo settings err: %v", err))
}
return to.TextResult(settings)
}
func GetUISettingsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetUISettingsFn")
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
settings, _, err := client.GetGlobalUISettings()
if err != nil {
return to.ErrorResult(fmt.Errorf("get UI settings err: %v", err))
}
return to.TextResult(settings)
}

View File

@@ -3,13 +3,15 @@ package timetracking
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
gitea_sdk "code.gitea.io/sdk/gitea" gitea_sdk "code.gitea.io/sdk/gitea"
"gitea.com/gitea/gitea-mcp/pkg/gitea" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/gitea"
"gitea.com/gitea/gitea-mcp/pkg/log" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/log"
"gitea.com/gitea/gitea-mcp/pkg/to" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/params"
"gitea.com/gitea/gitea-mcp/pkg/tool" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/to"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/tool"
"github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server" "github.com/mark3labs/mcp-go/server"
@@ -128,75 +130,75 @@ func StartStopwatchFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo
log.Debugf("Called StartStopwatchFn") log.Debugf("Called StartStopwatchFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
index, ok := req.GetArguments()["index"].(float64) index, err := params.GetIndex(req.GetArguments(), "index")
if !ok { if err != nil {
return to.ErrorResult(fmt.Errorf("index is required")) return to.ErrorResult(err)
} }
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
} }
_, err = client.StartIssueStopWatch(owner, repo, int64(index)) _, err = client.StartIssueStopWatch(owner, repo, index)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("start stopwatch on %s/%s#%d err: %v", owner, repo, int64(index), err)) return to.ErrorResult(fmt.Errorf("start stopwatch on %s/%s#%d err: %v", owner, repo, index, err))
} }
return to.TextResult(fmt.Sprintf("Stopwatch started on issue %s/%s#%d", owner, repo, int64(index))) return to.TextResult(fmt.Sprintf("Stopwatch started on issue %s/%s#%d", owner, repo, index))
} }
func StopStopwatchFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func StopStopwatchFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called StopStopwatchFn") log.Debugf("Called StopStopwatchFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
index, ok := req.GetArguments()["index"].(float64) index, err := params.GetIndex(req.GetArguments(), "index")
if !ok { if err != nil {
return to.ErrorResult(fmt.Errorf("index is required")) return to.ErrorResult(err)
} }
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
} }
_, err = client.StopIssueStopWatch(owner, repo, int64(index)) _, err = client.StopIssueStopWatch(owner, repo, index)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("stop stopwatch on %s/%s#%d err: %v", owner, repo, int64(index), err)) return to.ErrorResult(fmt.Errorf("stop stopwatch on %s/%s#%d err: %v", owner, repo, index, err))
} }
return to.TextResult(fmt.Sprintf("Stopwatch stopped on issue %s/%s#%d - time recorded", owner, repo, int64(index))) return to.TextResult(fmt.Sprintf("Stopwatch stopped on issue %s/%s#%d - time recorded", owner, repo, index))
} }
func DeleteStopwatchFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func DeleteStopwatchFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called DeleteStopwatchFn") log.Debugf("Called DeleteStopwatchFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
index, ok := req.GetArguments()["index"].(float64) index, err := params.GetIndex(req.GetArguments(), "index")
if !ok { if err != nil {
return to.ErrorResult(fmt.Errorf("index is required")) return to.ErrorResult(err)
} }
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
} }
_, err = client.DeleteIssueStopwatch(owner, repo, int64(index)) _, err = client.DeleteIssueStopwatch(owner, repo, index)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("delete stopwatch on %s/%s#%d err: %v", owner, repo, int64(index), err)) return to.ErrorResult(fmt.Errorf("delete stopwatch on %s/%s#%d err: %v", owner, repo, index, err))
} }
return to.TextResult(fmt.Sprintf("Stopwatch deleted/cancelled on issue %s/%s#%d", owner, repo, int64(index))) return to.TextResult(fmt.Sprintf("Stopwatch deleted/cancelled on issue %s/%s#%d", owner, repo, index))
} }
func GetMyStopwatchesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func GetMyStopwatchesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
@@ -205,7 +207,7 @@ func GetMyStopwatchesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
} }
stopwatches, _, err := client.GetMyStopwatches() stopwatches, _, err := client.ListMyStopwatches(gitea_sdk.ListStopwatchesOptions{})
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("get stopwatches err: %v", err)) return to.ErrorResult(fmt.Errorf("get stopwatches err: %v", err))
} }
@@ -221,40 +223,34 @@ func ListTrackedTimesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call
log.Debugf("Called ListTrackedTimesFn") log.Debugf("Called ListTrackedTimesFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
index, ok := req.GetArguments()["index"].(float64) index, err := params.GetIndex(req.GetArguments(), "index")
if !ok { if err != nil {
return to.ErrorResult(fmt.Errorf("index is required")) return to.ErrorResult(err)
}
page, ok := req.GetArguments()["page"].(float64)
if !ok {
page = 1
}
pageSize, ok := req.GetArguments()["pageSize"].(float64)
if !ok {
pageSize = 100
} }
page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
} }
times, _, err := client.ListIssueTrackedTimes(owner, repo, int64(index), gitea_sdk.ListTrackedTimesOptions{ times, _, err := client.ListIssueTrackedTimes(owner, repo, index, gitea_sdk.ListTrackedTimesOptions{
ListOptions: gitea_sdk.ListOptions{ ListOptions: gitea_sdk.ListOptions{
Page: int(page), Page: int(page),
PageSize: int(pageSize), PageSize: int(pageSize),
}, },
}) })
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("list tracked times for %s/%s#%d err: %v", owner, repo, int64(index), err)) return to.ErrorResult(fmt.Errorf("list tracked times for %s/%s#%d err: %v", owner, repo, index, err))
} }
if len(times) == 0 { if len(times) == 0 {
return to.TextResult(fmt.Sprintf("No tracked times for issue %s/%s#%d", owner, repo, int64(index))) return to.TextResult(fmt.Sprintf("No tracked times for issue %s/%s#%d", owner, repo, index))
} }
return to.TextResult(times) return to.TextResult(times)
} }
@@ -263,30 +259,30 @@ func AddTrackedTimeFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo
log.Debugf("Called AddTrackedTimeFn") log.Debugf("Called AddTrackedTimeFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
index, ok := req.GetArguments()["index"].(float64) index, err := params.GetIndex(req.GetArguments(), "index")
if !ok { if err != nil {
return to.ErrorResult(fmt.Errorf("index is required")) return to.ErrorResult(err)
} }
timeSeconds, ok := req.GetArguments()["time"].(float64) timeSeconds, err := params.GetIndex(req.GetArguments(), "time")
if !ok { if err != nil {
return to.ErrorResult(fmt.Errorf("time is required")) return to.ErrorResult(err)
} }
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
} }
trackedTime, _, err := client.AddTime(owner, repo, int64(index), gitea_sdk.AddTimeOption{ trackedTime, _, err := client.AddTime(owner, repo, index, gitea_sdk.AddTimeOption{
Time: int64(timeSeconds), Time: timeSeconds,
}) })
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("add tracked time to %s/%s#%d err: %v", owner, repo, int64(index), err)) return to.ErrorResult(fmt.Errorf("add tracked time to %s/%s#%d err: %v", owner, repo, index, err))
} }
return to.TextResult(trackedTime) return to.TextResult(trackedTime)
} }
@@ -295,51 +291,45 @@ func DeleteTrackedTimeFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Cal
log.Debugf("Called DeleteTrackedTimeFn") log.Debugf("Called DeleteTrackedTimeFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
index, ok := req.GetArguments()["index"].(float64) index, err := params.GetIndex(req.GetArguments(), "index")
if !ok { if err != nil {
return to.ErrorResult(fmt.Errorf("index is required")) return to.ErrorResult(err)
} }
id, ok := req.GetArguments()["id"].(float64) id, err := params.GetIndex(req.GetArguments(), "id")
if !ok { if err != nil {
return to.ErrorResult(fmt.Errorf("id is required")) return to.ErrorResult(err)
} }
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
} }
_, err = client.DeleteTime(owner, repo, int64(index), int64(id)) _, err = client.DeleteTime(owner, repo, index, id)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("delete tracked time %d from %s/%s#%d err: %v", int64(id), owner, repo, int64(index), err)) return to.ErrorResult(fmt.Errorf("delete tracked time %d from %s/%s#%d err: %v", id, owner, repo, index, err))
} }
return to.TextResult(fmt.Sprintf("Tracked time entry %d deleted from issue %s/%s#%d", int64(id), owner, repo, int64(index))) return to.TextResult(fmt.Sprintf("Tracked time entry %d deleted from issue %s/%s#%d", id, owner, repo, index))
} }
func ListRepoTimesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func ListRepoTimesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListRepoTimesFn") log.Debugf("Called ListRepoTimesFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
page, ok := req.GetArguments()["page"].(float64) page := params.GetOptionalInt(req.GetArguments(), "page", 1)
if !ok { pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
page = 1
}
pageSize, ok := req.GetArguments()["pageSize"].(float64)
if !ok {
pageSize = 100
}
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
@@ -365,7 +355,7 @@ func GetMyTimesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRe
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
} }
times, _, err := client.GetMyTrackedTimes() times, _, err := client.ListMyTrackedTimes(gitea_sdk.ListTrackedTimesOptions{})
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("get tracked times err: %v", err)) return to.ErrorResult(fmt.Errorf("get tracked times err: %v", err))
} }

125
operation/user/blocks.go Normal file
View File

@@ -0,0 +1,125 @@
package user
import (
"context"
"errors"
"fmt"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/gitea"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/log"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/params"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/to"
gitea_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
const (
ListMyBlocksToolName = "list_my_blocks"
CheckUserBlockToolName = "check_user_block"
BlockUserToolName = "block_user"
UnblockUserToolName = "unblock_user"
)
var (
ListMyBlocksTool = mcp.NewTool(
ListMyBlocksToolName,
mcp.WithDescription("List users blocked by the authenticated user"),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)),
)
CheckUserBlockTool = mcp.NewTool(
CheckUserBlockToolName,
mcp.WithDescription("Check if the authenticated user has blocked a user"),
mcp.WithString("username", mcp.Required(), mcp.Description("username to check")),
)
BlockUserTool = mcp.NewTool(
BlockUserToolName,
mcp.WithDescription("Block a user"),
mcp.WithString("username", mcp.Required(), mcp.Description("username to block")),
)
UnblockUserTool = mcp.NewTool(
UnblockUserToolName,
mcp.WithDescription("Unblock a user"),
mcp.WithString("username", mcp.Required(), mcp.Description("username to unblock")),
)
)
func init() {
Tool.RegisterRead(server.ServerTool{Tool: ListMyBlocksTool, Handler: ListMyBlocksFn})
Tool.RegisterRead(server.ServerTool{Tool: CheckUserBlockTool, Handler: CheckUserBlockFn})
Tool.RegisterWrite(server.ServerTool{Tool: BlockUserTool, Handler: BlockUserFn})
Tool.RegisterWrite(server.ServerTool{Tool: UnblockUserTool, Handler: UnblockUserFn})
}
func ListMyBlocksFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListMyBlocksFn")
page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
users, _, err := client.ListMyBlocks(gitea_sdk.ListUserBlocksOptions{
ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)},
})
if err != nil {
return to.ErrorResult(fmt.Errorf("list my blocks err: %v", err))
}
return to.TextResult(users)
}
func CheckUserBlockFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CheckUserBlockFn")
username, ok := req.GetArguments()["username"].(string)
if !ok {
return to.ErrorResult(errors.New("username is required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
isBlocked, _, err := client.CheckUserBlock(username)
if err != nil {
return to.ErrorResult(fmt.Errorf("check block %s err: %v", username, err))
}
return to.TextResult(map[string]any{"username": username, "is_blocked": isBlocked})
}
func BlockUserFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called BlockUserFn")
username, ok := req.GetArguments()["username"].(string)
if !ok {
return to.ErrorResult(errors.New("username is required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.BlockUser(username)
if err != nil {
return to.ErrorResult(fmt.Errorf("block user %s err: %v", username, err))
}
return to.TextResult(map[string]string{"status": "blocked", "username": username})
}
func UnblockUserFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called UnblockUserFn")
username, ok := req.GetArguments()["username"].(string)
if !ok {
return to.ErrorResult(errors.New("username is required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.UnblockUser(username)
if err != nil {
return to.ErrorResult(fmt.Errorf("unblock user %s err: %v", username, err))
}
return to.TextResult(map[string]string{"status": "unblocked", "username": username})
}

119
operation/user/emails.go Normal file
View File

@@ -0,0 +1,119 @@
package user
import (
"context"
"fmt"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/gitea"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/log"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/params"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/to"
gitea_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
const (
ListEmailsToolName = "list_emails"
AddEmailToolName = "add_email"
DeleteEmailToolName = "delete_email"
)
var (
ListEmailsTool = mcp.NewTool(
ListEmailsToolName,
mcp.WithDescription("List the authenticated user's email addresses"),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)),
)
AddEmailTool = mcp.NewTool(
AddEmailToolName,
mcp.WithDescription("Add email addresses for the authenticated user"),
mcp.WithArray("emails", mcp.Required(), mcp.Description("email addresses to add"), mcp.Items(map[string]any{"type": "string"})),
)
DeleteEmailTool = mcp.NewTool(
DeleteEmailToolName,
mcp.WithDescription("Delete email addresses for the authenticated user"),
mcp.WithArray("emails", mcp.Required(), mcp.Description("email addresses to delete"), mcp.Items(map[string]any{"type": "string"})),
)
)
func init() {
Tool.RegisterRead(server.ServerTool{Tool: ListEmailsTool, Handler: ListEmailsFn})
Tool.RegisterWrite(server.ServerTool{Tool: AddEmailTool, Handler: AddEmailFn})
Tool.RegisterWrite(server.ServerTool{Tool: DeleteEmailTool, Handler: DeleteEmailFn})
}
func ListEmailsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListEmailsFn")
page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
emails, _, err := client.ListEmails(gitea_sdk.ListEmailsOptions{
ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)},
})
if err != nil {
return to.ErrorResult(fmt.Errorf("list emails err: %v", err))
}
return to.TextResult(emails)
}
func AddEmailFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called AddEmailFn")
emailsArg, exists := req.GetArguments()["emails"]
if !exists {
return to.ErrorResult(fmt.Errorf("emails is required"))
}
emailsSlice, ok := emailsArg.([]any)
if !ok {
return to.ErrorResult(fmt.Errorf("emails must be an array"))
}
emails := make([]string, 0, len(emailsSlice))
for _, e := range emailsSlice {
if s, ok := e.(string); ok {
emails = append(emails, s)
}
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
result, _, err := client.AddEmail(gitea_sdk.CreateEmailOption{Emails: emails})
if err != nil {
return to.ErrorResult(fmt.Errorf("add emails err: %v", err))
}
return to.TextResult(result)
}
func DeleteEmailFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called DeleteEmailFn")
emailsArg, exists := req.GetArguments()["emails"]
if !exists {
return to.ErrorResult(fmt.Errorf("emails is required"))
}
emailsSlice, ok := emailsArg.([]any)
if !ok {
return to.ErrorResult(fmt.Errorf("emails must be an array"))
}
emails := make([]string, 0, len(emailsSlice))
for _, e := range emailsSlice {
if s, ok := e.(string); ok {
emails = append(emails, s)
}
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.DeleteEmail(gitea_sdk.DeleteEmailOption{Emails: emails})
if err != nil {
return to.ErrorResult(fmt.Errorf("delete emails err: %v", err))
}
return to.TextResult(map[string]any{"status": "deleted", "emails": emails})
}

210
operation/user/followers.go Normal file
View File

@@ -0,0 +1,210 @@
package user
import (
"context"
"errors"
"fmt"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/gitea"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/log"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/params"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/to"
gitea_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
const (
ListMyFollowersToolName = "list_my_followers"
ListMyFollowingToolName = "list_my_following"
ListUserFollowersToolName = "list_user_followers"
ListUserFollowingToolName = "list_user_following"
CheckFollowingToolName = "check_following"
FollowUserToolName = "follow_user"
UnfollowUserToolName = "unfollow_user"
)
var (
ListMyFollowersTool = mcp.NewTool(
ListMyFollowersToolName,
mcp.WithDescription("List the authenticated user's followers"),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)),
)
ListMyFollowingTool = mcp.NewTool(
ListMyFollowingToolName,
mcp.WithDescription("List users the authenticated user is following"),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)),
)
ListUserFollowersTool = mcp.NewTool(
ListUserFollowersToolName,
mcp.WithDescription("List a user's followers"),
mcp.WithString("username", mcp.Required(), mcp.Description("username")),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)),
)
ListUserFollowingTool = mcp.NewTool(
ListUserFollowingToolName,
mcp.WithDescription("List users a user is following"),
mcp.WithString("username", mcp.Required(), mcp.Description("username")),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)),
)
CheckFollowingTool = mcp.NewTool(
CheckFollowingToolName,
mcp.WithDescription("Check if the authenticated user is following a user"),
mcp.WithString("username", mcp.Required(), mcp.Description("username to check")),
)
FollowUserTool = mcp.NewTool(
FollowUserToolName,
mcp.WithDescription("Follow a user"),
mcp.WithString("username", mcp.Required(), mcp.Description("username to follow")),
)
UnfollowUserTool = mcp.NewTool(
UnfollowUserToolName,
mcp.WithDescription("Unfollow a user"),
mcp.WithString("username", mcp.Required(), mcp.Description("username to unfollow")),
)
)
func init() {
Tool.RegisterRead(server.ServerTool{Tool: ListMyFollowersTool, Handler: ListMyFollowersFn})
Tool.RegisterRead(server.ServerTool{Tool: ListMyFollowingTool, Handler: ListMyFollowingFn})
Tool.RegisterRead(server.ServerTool{Tool: ListUserFollowersTool, Handler: ListUserFollowersFn})
Tool.RegisterRead(server.ServerTool{Tool: ListUserFollowingTool, Handler: ListUserFollowingFn})
Tool.RegisterRead(server.ServerTool{Tool: CheckFollowingTool, Handler: CheckFollowingFn})
Tool.RegisterWrite(server.ServerTool{Tool: FollowUserTool, Handler: FollowUserFn})
Tool.RegisterWrite(server.ServerTool{Tool: UnfollowUserTool, Handler: UnfollowUserFn})
}
func ListMyFollowersFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListMyFollowersFn")
page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
followers, _, err := client.ListMyFollowers(gitea_sdk.ListFollowersOptions{
ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)},
})
if err != nil {
return to.ErrorResult(fmt.Errorf("list my followers err: %v", err))
}
return to.TextResult(followers)
}
func ListMyFollowingFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListMyFollowingFn")
page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
following, _, err := client.ListMyFollowing(gitea_sdk.ListFollowingOptions{
ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)},
})
if err != nil {
return to.ErrorResult(fmt.Errorf("list my following err: %v", err))
}
return to.TextResult(following)
}
func ListUserFollowersFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListUserFollowersFn")
username, ok := req.GetArguments()["username"].(string)
if !ok {
return to.ErrorResult(errors.New("username is required"))
}
page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
followers, _, err := client.ListFollowers(username, gitea_sdk.ListFollowersOptions{
ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)},
})
if err != nil {
return to.ErrorResult(fmt.Errorf("list %s followers err: %v", username, err))
}
return to.TextResult(followers)
}
func ListUserFollowingFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListUserFollowingFn")
username, ok := req.GetArguments()["username"].(string)
if !ok {
return to.ErrorResult(errors.New("username is required"))
}
page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
following, _, err := client.ListFollowing(username, gitea_sdk.ListFollowingOptions{
ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)},
})
if err != nil {
return to.ErrorResult(fmt.Errorf("list %s following err: %v", username, err))
}
return to.TextResult(following)
}
func CheckFollowingFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CheckFollowingFn")
username, ok := req.GetArguments()["username"].(string)
if !ok {
return to.ErrorResult(errors.New("username is required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
isFollowing, _ := client.IsFollowing(username)
return to.TextResult(map[string]any{"username": username, "is_following": isFollowing})
}
func FollowUserFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called FollowUserFn")
username, ok := req.GetArguments()["username"].(string)
if !ok {
return to.ErrorResult(errors.New("username is required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.Follow(username)
if err != nil {
return to.ErrorResult(fmt.Errorf("follow user %s err: %v", username, err))
}
return to.TextResult(map[string]string{"status": "following", "username": username})
}
func UnfollowUserFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called UnfollowUserFn")
username, ok := req.GetArguments()["username"].(string)
if !ok {
return to.ErrorResult(errors.New("username is required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.Unfollow(username)
if err != nil {
return to.ErrorResult(fmt.Errorf("unfollow user %s err: %v", username, err))
}
return to.TextResult(map[string]string{"status": "unfollowed", "username": username})
}

169
operation/user/keys.go Normal file
View File

@@ -0,0 +1,169 @@
package user
import (
"context"
"errors"
"fmt"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/gitea"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/log"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/params"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/to"
gitea_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
const (
ListMyPublicKeysToolName = "list_my_public_keys"
ListUserPublicKeysToolName = "list_user_public_keys"
GetPublicKeyToolName = "get_public_key"
CreatePublicKeyToolName = "create_public_key"
DeletePublicKeyToolName = "delete_public_key"
)
var (
ListMyPublicKeysTool = mcp.NewTool(
ListMyPublicKeysToolName,
mcp.WithDescription("List the authenticated user's SSH public keys"),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)),
)
ListUserPublicKeysTool = mcp.NewTool(
ListUserPublicKeysToolName,
mcp.WithDescription("List a user's SSH public keys"),
mcp.WithString("username", mcp.Required(), mcp.Description("username")),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)),
)
GetPublicKeyTool = mcp.NewTool(
GetPublicKeyToolName,
mcp.WithDescription("Get an SSH public key by ID"),
mcp.WithNumber("id", mcp.Required(), mcp.Description("key ID")),
)
CreatePublicKeyTool = mcp.NewTool(
CreatePublicKeyToolName,
mcp.WithDescription("Create an SSH public key for the authenticated user"),
mcp.WithString("title", mcp.Required(), mcp.Description("key title")),
mcp.WithString("key", mcp.Required(), mcp.Description("SSH public key content")),
mcp.WithBoolean("read_only", mcp.Description("whether the key is read-only")),
)
DeletePublicKeyTool = mcp.NewTool(
DeletePublicKeyToolName,
mcp.WithDescription("Delete an SSH public key"),
mcp.WithNumber("id", mcp.Required(), mcp.Description("key ID")),
)
)
func init() {
Tool.RegisterRead(server.ServerTool{Tool: ListMyPublicKeysTool, Handler: ListMyPublicKeysFn})
Tool.RegisterRead(server.ServerTool{Tool: ListUserPublicKeysTool, Handler: ListUserPublicKeysFn})
Tool.RegisterRead(server.ServerTool{Tool: GetPublicKeyTool, Handler: GetPublicKeyFn})
Tool.RegisterWrite(server.ServerTool{Tool: CreatePublicKeyTool, Handler: CreatePublicKeyFn})
Tool.RegisterWrite(server.ServerTool{Tool: DeletePublicKeyTool, Handler: DeletePublicKeyFn})
}
func ListMyPublicKeysFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListMyPublicKeysFn")
page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
keys, _, err := client.ListMyPublicKeys(gitea_sdk.ListPublicKeysOptions{
ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)},
})
if err != nil {
return to.ErrorResult(fmt.Errorf("list my public keys err: %v", err))
}
return to.TextResult(keys)
}
func ListUserPublicKeysFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListUserPublicKeysFn")
username, ok := req.GetArguments()["username"].(string)
if !ok {
return to.ErrorResult(errors.New("username is required"))
}
page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
keys, _, err := client.ListPublicKeys(username, gitea_sdk.ListPublicKeysOptions{
ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)},
})
if err != nil {
return to.ErrorResult(fmt.Errorf("list %s public keys err: %v", username, err))
}
return to.TextResult(keys)
}
func GetPublicKeyFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetPublicKeyFn")
id, err := params.GetIndex(req.GetArguments(), "id")
if err != nil {
return to.ErrorResult(err)
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
key, _, err := client.GetPublicKey(id)
if err != nil {
return to.ErrorResult(fmt.Errorf("get public key %d err: %v", id, err))
}
return to.TextResult(key)
}
func CreatePublicKeyFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CreatePublicKeyFn")
title, ok := req.GetArguments()["title"].(string)
if !ok {
return to.ErrorResult(errors.New("title is required"))
}
key, ok := req.GetArguments()["key"].(string)
if !ok {
return to.ErrorResult(errors.New("key is required"))
}
opt := gitea_sdk.CreateKeyOption{
Title: title,
Key: key,
}
if v, ok := req.GetArguments()["read_only"].(bool); ok {
opt.ReadOnly = v
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
pubKey, _, err := client.CreatePublicKey(opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("create public key err: %v", err))
}
return to.TextResult(pubKey)
}
func DeletePublicKeyFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called DeletePublicKeyFn")
id, err := params.GetIndex(req.GetArguments(), "id")
if err != nil {
return to.ErrorResult(err)
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.DeletePublicKey(id)
if err != nil {
return to.ErrorResult(fmt.Errorf("delete public key %d err: %v", id, err))
}
return to.TextResult(map[string]any{"status": "deleted", "key_id": id})
}

121
operation/user/profile.go Normal file
View File

@@ -0,0 +1,121 @@
package user
import (
"context"
"errors"
"fmt"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/gitea"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/log"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/to"
gitea_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
const (
GetUserInfoToolName = "get_user_info"
GetUserSettingsToolName = "get_user_settings"
UpdateUserSettingsToolName = "update_user_settings"
)
var (
GetUserInfoTool = mcp.NewTool(
GetUserInfoToolName,
mcp.WithDescription("Get a user's public information by username"),
mcp.WithString("username", mcp.Required(), mcp.Description("username")),
)
GetUserSettingsTool = mcp.NewTool(
GetUserSettingsToolName,
mcp.WithDescription("Get the authenticated user's settings"),
)
UpdateUserSettingsTool = mcp.NewTool(
UpdateUserSettingsToolName,
mcp.WithDescription("Update the authenticated user's settings"),
mcp.WithString("full_name", mcp.Description("full name")),
mcp.WithString("description", mcp.Description("user bio/description")),
mcp.WithString("website", mcp.Description("website URL")),
mcp.WithString("location", mcp.Description("location")),
mcp.WithString("language", mcp.Description("language preference")),
mcp.WithString("theme", mcp.Description("UI theme")),
mcp.WithBoolean("hide_email", mcp.Description("hide email from profile")),
mcp.WithBoolean("hide_activity", mcp.Description("hide activity from profile")),
)
)
func init() {
Tool.RegisterRead(server.ServerTool{Tool: GetUserInfoTool, Handler: GetUserByNameFn})
Tool.RegisterRead(server.ServerTool{Tool: GetUserSettingsTool, Handler: GetUserSettingsFn})
Tool.RegisterWrite(server.ServerTool{Tool: UpdateUserSettingsTool, Handler: UpdateUserSettingsFn})
}
func GetUserByNameFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetUserByNameFn")
username, ok := req.GetArguments()["username"].(string)
if !ok {
return to.ErrorResult(errors.New("username is required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
user, _, err := client.GetUserInfo(username)
if err != nil {
return to.ErrorResult(fmt.Errorf("get user %s err: %v", username, err))
}
return to.TextResult(user)
}
func GetUserSettingsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetUserSettingsFn")
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
settings, _, err := client.GetUserSettings()
if err != nil {
return to.ErrorResult(fmt.Errorf("get user settings err: %v", err))
}
return to.TextResult(settings)
}
func UpdateUserSettingsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called UpdateUserSettingsFn")
opt := gitea_sdk.UserSettingsOptions{}
if v, ok := req.GetArguments()["full_name"].(string); ok {
opt.FullName = &v
}
if v, ok := req.GetArguments()["description"].(string); ok {
opt.Description = &v
}
if v, ok := req.GetArguments()["website"].(string); ok {
opt.Website = &v
}
if v, ok := req.GetArguments()["location"].(string); ok {
opt.Location = &v
}
if v, ok := req.GetArguments()["language"].(string); ok {
opt.Language = &v
}
if v, ok := req.GetArguments()["theme"].(string); ok {
opt.Theme = &v
}
if v, ok := req.GetArguments()["hide_email"].(bool); ok {
opt.HideEmail = &v
}
if v, ok := req.GetArguments()["hide_activity"].(bool); ok {
opt.HideActivity = &v
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
settings, _, err := client.UpdateUserSettings(opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("update user settings err: %v", err))
}
return to.TextResult(settings)
}

254
operation/user/repos.go Normal file
View File

@@ -0,0 +1,254 @@
package user
import (
"context"
"errors"
"fmt"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/gitea"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/log"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/params"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/to"
gitea_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
const (
ListUserReposToolName = "list_user_repos"
ListMyStarredToolName = "list_my_starred"
StarRepoToolName = "star_repo"
UnstarRepoToolName = "unstar_repo"
CheckStarredToolName = "check_starred"
ListMySubscriptionsToolName = "list_my_subscriptions"
ListUserHeatmapToolName = "list_user_heatmap"
ListUserActivityToolName = "list_user_activity"
)
var (
ListUserReposTool = mcp.NewTool(
ListUserReposToolName,
mcp.WithDescription("List a user's repositories"),
mcp.WithString("username", mcp.Required(), mcp.Description("username")),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)),
)
ListMyStarredTool = mcp.NewTool(
ListMyStarredToolName,
mcp.WithDescription("List repositories starred by the authenticated user"),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)),
)
StarRepoTool = mcp.NewTool(
StarRepoToolName,
mcp.WithDescription("Star a repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
)
UnstarRepoTool = mcp.NewTool(
UnstarRepoToolName,
mcp.WithDescription("Unstar a repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
)
CheckStarredTool = mcp.NewTool(
CheckStarredToolName,
mcp.WithDescription("Check if the authenticated user has starred a repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
)
ListMySubscriptionsTool = mcp.NewTool(
ListMySubscriptionsToolName,
mcp.WithDescription("List repositories watched by the authenticated user"),
)
ListUserHeatmapTool = mcp.NewTool(
ListUserHeatmapToolName,
mcp.WithDescription("Get a user's contribution heatmap data"),
mcp.WithString("username", mcp.Required(), mcp.Description("username")),
)
ListUserActivityTool = mcp.NewTool(
ListUserActivityToolName,
mcp.WithDescription("List a user's activity feeds"),
mcp.WithString("username", mcp.Required(), mcp.Description("username")),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)),
)
)
func init() {
Tool.RegisterRead(server.ServerTool{Tool: ListUserReposTool, Handler: ListUserReposFn})
Tool.RegisterRead(server.ServerTool{Tool: ListMyStarredTool, Handler: ListMyStarredFn})
Tool.RegisterRead(server.ServerTool{Tool: CheckStarredTool, Handler: CheckStarredFn})
Tool.RegisterRead(server.ServerTool{Tool: ListMySubscriptionsTool, Handler: ListMySubscriptionsFn})
Tool.RegisterRead(server.ServerTool{Tool: ListUserHeatmapTool, Handler: ListUserHeatmapFn})
Tool.RegisterRead(server.ServerTool{Tool: ListUserActivityTool, Handler: ListUserActivityFn})
Tool.RegisterWrite(server.ServerTool{Tool: StarRepoTool, Handler: StarRepoFn})
Tool.RegisterWrite(server.ServerTool{Tool: UnstarRepoTool, Handler: UnstarRepoFn})
}
func ListUserReposFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListUserReposFn")
username, ok := req.GetArguments()["username"].(string)
if !ok {
return to.ErrorResult(errors.New("username is required"))
}
page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
repos, _, err := client.ListUserRepos(username, gitea_sdk.ListReposOptions{
ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)},
})
if err != nil {
return to.ErrorResult(fmt.Errorf("list %s repos err: %v", username, err))
}
return to.TextResult(repos)
}
func ListMyStarredFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListMyStarredFn")
page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
repos, _, err := client.GetMyStarredRepos()
if err != nil {
return to.ErrorResult(fmt.Errorf("list my starred repos err: %v", err))
}
// Manual pagination since SDK returns all
start := (int(page) - 1) * int(pageSize)
if start >= len(repos) {
return to.TextResult([]*gitea_sdk.Repository{})
}
end := start + int(pageSize)
if end > len(repos) {
end = len(repos)
}
return to.TextResult(repos[start:end])
}
func StarRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called StarRepoFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(errors.New("owner is required"))
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(errors.New("repo is required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.StarRepo(owner, repo)
if err != nil {
return to.ErrorResult(fmt.Errorf("star %s/%s err: %v", owner, repo, err))
}
return to.TextResult(map[string]string{"status": "starred", "owner": owner, "repo": repo})
}
func UnstarRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called UnstarRepoFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(errors.New("owner is required"))
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(errors.New("repo is required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.UnStarRepo(owner, repo)
if err != nil {
return to.ErrorResult(fmt.Errorf("unstar %s/%s err: %v", owner, repo, err))
}
return to.TextResult(map[string]string{"status": "unstarred", "owner": owner, "repo": repo})
}
func CheckStarredFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CheckStarredFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(errors.New("owner is required"))
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(errors.New("repo is required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
isStarred, _, err := client.IsRepoStarring(owner, repo)
if err != nil {
return to.ErrorResult(fmt.Errorf("check starred %s/%s err: %v", owner, repo, err))
}
return to.TextResult(map[string]any{"owner": owner, "repo": repo, "is_starred": isStarred})
}
func ListMySubscriptionsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListMySubscriptionsFn")
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
repos, _, err := client.GetMyWatchedRepos()
if err != nil {
return to.ErrorResult(fmt.Errorf("list my subscriptions err: %v", err))
}
return to.TextResult(repos)
}
func ListUserHeatmapFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListUserHeatmapFn")
username, ok := req.GetArguments()["username"].(string)
if !ok {
return to.ErrorResult(errors.New("username is required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
heatmap, _, err := client.GetUserHeatmap(username)
if err != nil {
return to.ErrorResult(fmt.Errorf("get %s heatmap err: %v", username, err))
}
return to.TextResult(heatmap)
}
func ListUserActivityFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListUserActivityFn")
username, ok := req.GetArguments()["username"].(string)
if !ok {
return to.ErrorResult(errors.New("username is required"))
}
page := params.GetOptionalInt(req.GetArguments(), "page", 1)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100)
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
feeds, _, err := client.ListUserActivityFeeds(username, gitea_sdk.ListUserActivityFeedsOptions{
ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)},
})
if err != nil {
return to.ErrorResult(fmt.Errorf("list %s activity err: %v", username, err))
}
return to.TextResult(feeds)
}

View File

@@ -4,10 +4,11 @@ import (
"context" "context"
"fmt" "fmt"
"gitea.com/gitea/gitea-mcp/pkg/gitea" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/gitea"
"gitea.com/gitea/gitea-mcp/pkg/log" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/log"
"gitea.com/gitea/gitea-mcp/pkg/to" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/params"
"gitea.com/gitea/gitea-mcp/pkg/tool" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/to"
"git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/tool"
gitea_sdk "code.gitea.io/sdk/gitea" gitea_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/mcp"
@@ -68,11 +69,11 @@ func registerTools() {
// getIntArg parses an integer argument from the MCP request arguments map. // getIntArg parses an integer argument from the MCP request arguments map.
// Returns def if missing, not a number, or less than 1. Used for pagination arguments. // Returns def if missing, not a number, or less than 1. Used for pagination arguments.
func getIntArg(req mcp.CallToolRequest, name string, def int) int { func getIntArg(req mcp.CallToolRequest, name string, def int) int {
val, ok := req.GetArguments()[name].(float64) v := params.GetOptionalInt(req.GetArguments(), name, int64(def))
if !ok || val < 1 { if v < 1 {
return def return def
} }
return int(val) return int(v)
} }
// GetUserInfoFn is the handler for "get_my_user_info" MCP tool requests. // GetUserInfoFn is the handler for "get_my_user_info" MCP tool requests.

View File

@@ -4,10 +4,10 @@ import (
"context" "context"
"fmt" "fmt"
"gitea.com/gitea/gitea-mcp/pkg/flag" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/flag"
"gitea.com/gitea/gitea-mcp/pkg/log" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/log"
"gitea.com/gitea/gitea-mcp/pkg/to" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/to"
"gitea.com/gitea/gitea-mcp/pkg/tool" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/tool"
"github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server" "github.com/mark3labs/mcp-go/server"

View File

@@ -2,13 +2,14 @@ package wiki
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"net/url" "net/url"
"gitea.com/gitea/gitea-mcp/pkg/gitea" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/gitea"
"gitea.com/gitea/gitea-mcp/pkg/log" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/log"
"gitea.com/gitea/gitea-mcp/pkg/to" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/to"
"gitea.com/gitea/gitea-mcp/pkg/tool" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/tool"
"github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server" "github.com/mark3labs/mcp-go/server"
@@ -110,11 +111,11 @@ func ListWikiPagesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToo
log.Debugf("Called ListWikiPagesFn") log.Debugf("Called ListWikiPagesFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
// Use direct HTTP request because SDK does not support yet wikis // Use direct HTTP request because SDK does not support yet wikis
@@ -131,15 +132,15 @@ func GetWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolR
log.Debugf("Called GetWikiPageFn") log.Debugf("Called GetWikiPageFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
pageName, ok := req.GetArguments()["pageName"].(string) pageName, ok := req.GetArguments()["pageName"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("pageName is required")) return to.ErrorResult(errors.New("pageName is required"))
} }
var result any var result any
@@ -155,15 +156,15 @@ func GetWikiRevisionsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call
log.Debugf("Called GetWikiRevisionsFn") log.Debugf("Called GetWikiRevisionsFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
pageName, ok := req.GetArguments()["pageName"].(string) pageName, ok := req.GetArguments()["pageName"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("pageName is required")) return to.ErrorResult(errors.New("pageName is required"))
} }
var result any var result any
@@ -179,19 +180,19 @@ func CreateWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo
log.Debugf("Called CreateWikiPageFn") log.Debugf("Called CreateWikiPageFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
title, ok := req.GetArguments()["title"].(string) title, ok := req.GetArguments()["title"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("title is required")) return to.ErrorResult(errors.New("title is required"))
} }
contentBase64, ok := req.GetArguments()["content_base64"].(string) contentBase64, ok := req.GetArguments()["content_base64"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("content_base64 is required")) return to.ErrorResult(errors.New("content_base64 is required"))
} }
message, _ := req.GetArguments()["message"].(string) message, _ := req.GetArguments()["message"].(string)
@@ -218,19 +219,19 @@ func UpdateWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo
log.Debugf("Called UpdateWikiPageFn") log.Debugf("Called UpdateWikiPageFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
pageName, ok := req.GetArguments()["pageName"].(string) pageName, ok := req.GetArguments()["pageName"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("pageName is required")) return to.ErrorResult(errors.New("pageName is required"))
} }
contentBase64, ok := req.GetArguments()["content_base64"].(string) contentBase64, ok := req.GetArguments()["content_base64"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("content_base64 is required")) return to.ErrorResult(errors.New("content_base64 is required"))
} }
requestBody := map[string]string{ requestBody := map[string]string{
@@ -264,15 +265,15 @@ func DeleteWikiPageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo
log.Debugf("Called DeleteWikiPageFn") log.Debugf("Called DeleteWikiPageFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("owner is required")) return to.ErrorResult(errors.New("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("repo is required")) return to.ErrorResult(errors.New("repo is required"))
} }
pageName, ok := req.GetArguments()["pageName"].(string) pageName, ok := req.GetArguments()["pageName"].(string)
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("pageName is required")) return to.ErrorResult(errors.New("pageName is required"))
} }
_, err := gitea.DoJSON(ctx, "DELETE", fmt.Sprintf("repos/%s/%s/wiki/page/%s", url.QueryEscape(owner), url.QueryEscape(repo), url.QueryEscape(pageName)), nil, nil, nil) _, err := gitea.DoJSON(ctx, "DELETE", fmt.Sprintf("repos/%s/%s/wiki/page/%s", url.QueryEscape(owner), url.QueryEscape(repo), url.QueryEscape(pageName)), nil, nil, nil)

View File

@@ -7,8 +7,8 @@ import (
"net/http" "net/http"
"code.gitea.io/sdk/gitea" "code.gitea.io/sdk/gitea"
mcpContext "gitea.com/gitea/gitea-mcp/pkg/context" mcpContext "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/context"
"gitea.com/gitea/gitea-mcp/pkg/flag" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/flag"
) )
func NewClient(token string) (*gitea.Client, error) { func NewClient(token string) (*gitea.Client, error) {
@@ -34,7 +34,7 @@ func NewClient(token string) (*gitea.Client, error) {
} }
// Set user agent for the client // Set user agent for the client
client.SetUserAgent(fmt.Sprintf("gitea-mcp-server/%s", flag.Version)) client.SetUserAgent("gitea-mcp-server/" + flag.Version)
return client, nil return client, nil
} }

View File

@@ -5,6 +5,7 @@ import (
"context" "context"
"crypto/tls" "crypto/tls"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
@@ -12,8 +13,8 @@ import (
"strings" "strings"
"time" "time"
mcpContext "gitea.com/gitea/gitea-mcp/pkg/context" mcpContext "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/context"
"gitea.com/gitea/gitea-mcp/pkg/flag" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/flag"
) )
type HTTPError struct { type HTTPError struct {
@@ -40,7 +41,7 @@ func tokenFromContext(ctx context.Context) string {
func newRESTHTTPClient() *http.Client { func newRESTHTTPClient() *http.Client {
transport := http.DefaultTransport.(*http.Transport).Clone() transport := http.DefaultTransport.(*http.Transport).Clone()
if flag.Insecure { if flag.Insecure {
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} //nolint:gosec transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} //nolint:gosec // user-requested insecure mode
} }
return &http.Client{ return &http.Client{
Transport: transport, Transport: transport,
@@ -51,7 +52,7 @@ func newRESTHTTPClient() *http.Client {
func buildAPIURL(path string, query url.Values) (string, error) { func buildAPIURL(path string, query url.Values) (string, error) {
host := strings.TrimRight(flag.Host, "/") host := strings.TrimRight(flag.Host, "/")
if host == "" { if host == "" {
return "", fmt.Errorf("gitea host is empty") return "", errors.New("gitea host is empty")
} }
p := strings.TrimLeft(path, "/") p := strings.TrimLeft(path, "/")
u, err := url.Parse(fmt.Sprintf("%s/api/v1/%s", host, p)) u, err := url.Parse(fmt.Sprintf("%s/api/v1/%s", host, p))
@@ -66,7 +67,7 @@ func buildAPIURL(path string, query url.Values) (string, error) {
// DoJSON performs an API request and decodes a JSON response into respOut (if non-nil). // DoJSON performs an API request and decodes a JSON response into respOut (if non-nil).
// It returns the HTTP status code. // It returns the HTTP status code.
func DoJSON(ctx context.Context, method, path string, query url.Values, body any, respOut any) (int, error) { func DoJSON(ctx context.Context, method, path string, query url.Values, body, respOut any) (int, error) {
var bodyReader io.Reader var bodyReader io.Reader
if body != nil { if body != nil {
b, err := json.Marshal(body) b, err := json.Marshal(body)
@@ -87,7 +88,7 @@ func DoJSON(ctx context.Context, method, path string, query url.Values, body any
token := tokenFromContext(ctx) token := tokenFromContext(ctx)
if token != "" { if token != "" {
req.Header.Set("Authorization", fmt.Sprintf("token %s", token)) req.Header.Set("Authorization", "token "+token)
} }
req.Header.Set("Accept", "application/json") req.Header.Set("Accept", "application/json")
if body != nil { if body != nil {
@@ -107,7 +108,7 @@ func DoJSON(ctx context.Context, method, path string, query url.Values, body any
} }
if respOut == nil { if respOut == nil {
io.Copy(io.Discard, resp.Body) // best-effort _, _ = io.Copy(io.Discard, resp.Body) // best-effort
return resp.StatusCode, nil return resp.StatusCode, nil
} }
@@ -140,7 +141,7 @@ func DoBytes(ctx context.Context, method, path string, query url.Values, body an
token := tokenFromContext(ctx) token := tokenFromContext(ctx)
if token != "" { if token != "" {
req.Header.Set("Authorization", fmt.Sprintf("token %s", token)) req.Header.Set("Authorization", "token "+token)
} }
if accept != "" { if accept != "" {
req.Header.Set("Accept", accept) req.Header.Set("Accept", accept)
@@ -171,5 +172,3 @@ func DoBytes(ctx context.Context, method, path string, query url.Values, body an
return respBytes, resp.StatusCode, nil return respBytes, resp.StatusCode, nil
} }

View File

@@ -4,8 +4,8 @@ import (
"context" "context"
"testing" "testing"
mcpContext "gitea.com/gitea/gitea-mcp/pkg/context" mcpContext "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/context"
"gitea.com/gitea/gitea-mcp/pkg/flag" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/flag"
) )
func TestTokenFromContext(t *testing.T) { func TestTokenFromContext(t *testing.T) {
@@ -28,5 +28,3 @@ func TestTokenFromContext(t *testing.T) {
} }
}) })
} }

View File

@@ -1,12 +1,11 @@
package log package log
import ( import (
"fmt"
"os" "os"
"sync" "sync"
"time" "time"
"gitea.com/gitea/gitea-mcp/pkg/flag" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/flag"
"go.uber.org/zap" "go.uber.org/zap"
"go.uber.org/zap/zapcore" "go.uber.org/zap/zapcore"
"gopkg.in/natefinch/lumberjack.v2" "gopkg.in/natefinch/lumberjack.v2"
@@ -35,14 +34,14 @@ func Default() *zap.Logger {
home = os.TempDir() home = os.TempDir()
} }
logDir := fmt.Sprintf("%s/.gitea-mcp", home) logDir := home + "/.gitea-mcp"
if err := os.MkdirAll(logDir, 0o700); err != nil { if err := os.MkdirAll(logDir, 0o700); err != nil {
// Fallback to temp directory if creation fails // Fallback to temp directory if creation fails
logDir = os.TempDir() logDir = os.TempDir()
} }
wss = append(wss, zapcore.AddSync(&lumberjack.Logger{ wss = append(wss, zapcore.AddSync(&lumberjack.Logger{
Filename: fmt.Sprintf("%s/gitea-mcp.log", logDir), Filename: logDir + "/gitea-mcp.log",
MaxSize: 100, MaxSize: 100,
MaxBackups: 10, MaxBackups: 10,
MaxAge: 30, MaxAge: 30,

58
pkg/params/params.go Normal file
View File

@@ -0,0 +1,58 @@
package params
import (
"fmt"
"strconv"
)
// ToInt64 converts a value to int64, accepting both float64 (JSON number) and
// string representations. Returns false if the value cannot be converted.
func ToInt64(val any) (int64, bool) {
switch v := val.(type) {
case float64:
return int64(v), true
case string:
i, err := strconv.ParseInt(v, 10, 64)
if err != nil {
return 0, false
}
return i, true
default:
return 0, false
}
}
// GetIndex extracts a required integer parameter from MCP tool arguments.
// It accepts both numeric (float64 from JSON) and string representations.
// This provides better UX for LLM callers that may naturally use strings
// for identifiers like issue/PR numbers.
func GetIndex(args map[string]any, key string) (int64, error) {
val, exists := args[key]
if !exists {
return 0, fmt.Errorf("%s is required", key)
}
if i, ok := ToInt64(val); ok {
return i, nil
}
if s, ok := val.(string); ok {
return 0, fmt.Errorf("%s must be a valid integer (got %q)", key, s)
}
return 0, fmt.Errorf("%s must be a number or numeric string", key)
}
// GetOptionalInt extracts an optional integer parameter from MCP tool arguments.
// Returns defaultVal if the key is missing or the value cannot be parsed.
// Accepts both float64 (JSON number) and string representations.
func GetOptionalInt(args map[string]any, key string, defaultVal int64) int64 {
val, exists := args[key]
if !exists {
return defaultVal
}
if i, ok := ToInt64(val); ok {
return i
}
return defaultVal
}

161
pkg/params/params_test.go Normal file
View File

@@ -0,0 +1,161 @@
package params
import (
"strings"
"testing"
)
func TestToInt64(t *testing.T) {
tests := []struct {
name string
val any
want int64
ok bool
}{
{"float64", float64(42), 42, true},
{"float64 zero", float64(0), 0, true},
{"float64 negative", float64(-5), -5, true},
{"string", "123", 123, true},
{"string zero", "0", 0, true},
{"string negative", "-10", -10, true},
{"invalid string", "abc", 0, false},
{"decimal string", "1.5", 0, false},
{"bool", true, 0, false},
{"nil", nil, 0, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, ok := ToInt64(tt.val)
if ok != tt.ok {
t.Errorf("ToInt64() ok = %v, want %v", ok, tt.ok)
}
if got != tt.want {
t.Errorf("ToInt64() = %v, want %v", got, tt.want)
}
})
}
}
func TestGetOptionalInt(t *testing.T) {
tests := []struct {
name string
args map[string]any
key string
defaultVal int64
want int64
}{
{"present float64", map[string]any{"page": float64(3)}, "page", 1, 3},
{"present string", map[string]any{"page": "5"}, "page", 1, 5},
{"missing key", map[string]any{}, "page", 1, 1},
{"invalid string", map[string]any{"page": "abc"}, "page", 1, 1},
{"invalid type", map[string]any{"page": true}, "page", 1, 1},
{"zero value", map[string]any{"id": float64(0)}, "id", 99, 0},
{"string zero", map[string]any{"id": "0"}, "id", 99, 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := GetOptionalInt(tt.args, tt.key, tt.defaultVal)
if got != tt.want {
t.Errorf("GetOptionalInt() = %v, want %v", got, tt.want)
}
})
}
}
func TestGetIndex(t *testing.T) {
tests := []struct {
name string
args map[string]any
key string
wantIndex int64
wantErr bool
errMsg string
}{
{
name: "valid float64",
args: map[string]any{"index": float64(123)},
key: "index",
wantIndex: 123,
wantErr: false,
},
{
name: "valid string",
args: map[string]any{"index": "456"},
key: "index",
wantIndex: 456,
wantErr: false,
},
{
name: "valid string with large number",
args: map[string]any{"index": "999999"},
key: "index",
wantIndex: 999999,
wantErr: false,
},
{
name: "missing parameter",
args: map[string]any{},
key: "index",
wantErr: true,
errMsg: "index is required",
},
{
name: "invalid string (not a number)",
args: map[string]any{"index": "abc"},
key: "index",
wantErr: true,
errMsg: "must be a valid integer",
},
{
name: "invalid string (decimal)",
args: map[string]any{"index": "12.34"},
key: "index",
wantErr: true,
errMsg: "must be a valid integer",
},
{
name: "invalid type (bool)",
args: map[string]any{"index": true},
key: "index",
wantErr: true,
errMsg: "must be a number or numeric string",
},
{
name: "invalid type (map)",
args: map[string]any{"index": map[string]string{"foo": "bar"}},
key: "index",
wantErr: true,
errMsg: "must be a number or numeric string",
},
{
name: "custom key name",
args: map[string]any{"pr_index": "789"},
key: "pr_index",
wantIndex: 789,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotIndex, err := GetIndex(tt.args, tt.key)
if tt.wantErr {
if err == nil {
t.Errorf("GetIndex() expected error but got nil")
return
}
if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) {
t.Errorf("GetIndex() error = %v, want error containing %q", err, tt.errMsg)
}
return
}
if err != nil {
t.Errorf("GetIndex() unexpected error = %v", err)
return
}
if gotIndex != tt.wantIndex {
t.Errorf("GetIndex() = %v, want %v", gotIndex, tt.wantIndex)
}
})
}
}

View File

@@ -1,73 +0,0 @@
/*
Copyright 2023 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package ptr
import (
"fmt"
"reflect"
)
// AllPtrFieldsNil tests whether all pointer fields in a struct are nil. This is useful when,
// for example, an API struct is handled by plugins which need to distinguish
// "no plugin accepted this spec" from "this spec is empty".
//
// This function is only valid for structs and pointers to structs. Any other
// type will cause a panic. Passing a typed nil pointer will return true.
func AllPtrFieldsNil(obj interface{}) bool {
v := reflect.ValueOf(obj)
if !v.IsValid() {
panic(fmt.Sprintf("reflect.ValueOf() produced a non-valid Value for %#v", obj))
}
if v.Kind() == reflect.Ptr {
if v.IsNil() {
return true
}
v = v.Elem()
}
for i := 0; i < v.NumField(); i++ {
if v.Field(i).Kind() == reflect.Ptr && !v.Field(i).IsNil() {
return false
}
}
return true
}
// To returns a pointer to the given value.
func To[T any](v T) *T {
return &v
}
// Deref dereferences ptr and returns the value it points to if no nil, or else
// returns def.
func Deref[T any](ptr *T, def T) T {
if ptr != nil {
return *ptr
}
return def
}
// Equal returns true if both arguments are nil or both arguments
// dereference to the same value.
func Equal[T comparable](a, b *T) bool {
if (a == nil) != (b == nil) {
return false
}
if a == nil {
return true
}
return *a == *b
}

View File

@@ -4,7 +4,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"gitea.com/gitea/gitea-mcp/pkg/log" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/log"
"github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/mcp"
) )

View File

@@ -1,7 +1,7 @@
package tool package tool
import ( import (
"gitea.com/gitea/gitea-mcp/pkg/flag" "git.lethalbits.com/lethalbits/gitea-mcp-extended/pkg/flag"
"github.com/mark3labs/mcp-go/server" "github.com/mark3labs/mcp-go/server"
) )