3 Commits

Author SHA1 Message Date
silverwind
1620f1d6a9 refactor(test): use strings.Contains directly instead of wrapper
Remove the custom contains() wrapper function in params_test.go
and use strings.Contains directly as suggested in review.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 18:57:35 +01:00
James Pharaoh
b8630b5b9a refactor: simplify test helper using stdlib strings.Contains
Addresses code review feedback from silverwind:
- Replace complex custom contains() implementation with strings.Contains
- Remove unnecessary containsMiddle() function
- Improves readability and maintainability by using standard library

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-10 12:24:44 +00:00
James Pharaoh
71dbc9d6da feat: accept string or number for index parameters (#121)
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

This improves UX for MCP clients and LLMs while maintaining backward
compatibility with existing numeric callers.

Fixes: #121

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-10 09:23:45 +00:00
44 changed files with 1109 additions and 1626 deletions

View File

@@ -14,7 +14,7 @@ jobs:
DOCKER_LATEST: nightly DOCKER_LATEST: nightly
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v4
with: with:
fetch-depth: 0 # all history for all branches and tags fetch-depth: 0 # all history for all branches and tags
@@ -37,7 +37,7 @@ jobs:
echo REPO_VERSION=$(git describe --tags --always | sed 's/-/+/' | sed 's/^v//') >> $GITHUB_OUTPUT echo REPO_VERSION=$(git describe --tags --always | sed 's/-/+/' | sed 's/^v//') >> $GITHUB_OUTPUT
- name: Build and push - name: Build and push
uses: docker/build-push-action@v6 uses: docker/build-push-action@v5
with: with:
context: . context: .
file: ./Dockerfile file: ./Dockerfile

View File

@@ -10,17 +10,20 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v6 uses: actions/setup-go@v5
with: with:
go-version: stable go-version: stable
- name: Install GoReleaser
run: go install github.com/goreleaser/goreleaser/v2@latest
- name: Run GoReleaser - name: Run GoReleaser
run: goreleaser release --clean uses: goreleaser/goreleaser-action@v6
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"
@@ -32,7 +35,7 @@ jobs:
DOCKER_LATEST: latest DOCKER_LATEST: latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v4
with: with:
fetch-depth: 0 # all history for all branches and tags fetch-depth: 0 # all history for all branches and tags
@@ -55,7 +58,7 @@ jobs:
echo REPO_VERSION=${GITHUB_REF_NAME#v} >> $GITHUB_OUTPUT echo REPO_VERSION=${GITHUB_REF_NAME#v} >> $GITHUB_OUTPUT
- name: Build and push - name: Build and push
uses: docker/build-push-action@v6 uses: docker/build-push-action@v5
with: with:
context: . context: .
file: ./Dockerfile file: ./Dockerfile

View File

@@ -7,13 +7,20 @@ jobs:
check-and-test: check-and-test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- uses: actions/setup-go@v6 - uses: actions/setup-go@v5
with: with:
go-version-file: 'go.mod' go-version-file: 'go.mod'
- name: lint
run: make lint
- name: build - name: build
run: make build run: |
- name: security-check make build
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: ./...

View File

@@ -1,113 +0,0 @@
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

@@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1.4 # syntax=docker/dockerfile:1.4
# Build stage # Build stage
FROM --platform=$BUILDPLATFORM golang:1.26-alpine AS builder FROM --platform=$BUILDPLATFORM golang:1.24-alpine AS builder
ARG VERSION=dev ARG VERSION=dev
ARG TARGETOS ARG TARGETOS

View File

@@ -3,10 +3,6 @@ EXECUTABLE := gitea-mcp
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]"
@@ -49,29 +45,8 @@ air: ## Install air for hot reload.
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
.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 ## tidy and verify module dependencies vendor: ## tidy and verify module dependencies
$(GO) mod verify @echo 'Tidying and verifying module dependencies...'
go mod tidy
go mod verify

View File

@@ -221,7 +221,6 @@ 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 |

View File

@@ -220,7 +220,6 @@ 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

@@ -220,7 +220,6 @@ 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

@@ -99,12 +99,12 @@ func init() {
} }
func Execute() { func Execute() {
defer log.Default().Sync() //nolint:errcheck // best-effort flush defer log.Default().Sync()
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) //nolint:gocritic // intentional exit after defer log.Fatalf("Run Gitea MCP Server Error: %v", err)
} }
} }

14
go.mod
View File

@@ -1,11 +1,11 @@
module gitea.com/gitea/gitea-mcp module gitea.com/gitea/gitea-mcp
go 1.26.0 go 1.24.0
require ( require (
code.gitea.io/sdk/gitea v0.23.2 code.gitea.io/sdk/gitea v0.22.1
github.com/mark3labs/mcp-go v0.44.0 github.com/mark3labs/mcp-go v0.42.0
go.uber.org/zap v1.27.1 go.uber.org/zap v1.27.0
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.8.0 // indirect github.com/hashicorp/go-version v1.7.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.48.0 // indirect golang.org/x/crypto v0.43.0 // indirect
golang.org/x/sys v0.41.0 // indirect golang.org/x/sys v0.37.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.23.2 h1:iJB1FDmLegwfwjX8gotBDHdPSbk/ZR8V9VmEJaVsJYg= code.gitea.io/sdk/gitea v0.22.1 h1:7K05KjRORyTcTYULQ/AwvlVS6pawLcWyXZcTr7gHFyA=
code.gitea.io/sdk/gitea v0.23.2/go.mod h1:yyF5+GhljqvA30sRDreoyHILruNiy4ASufugzYg0VHM= code.gitea.io/sdk/gitea v0.22.1/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.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4= github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go-version v1.7.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.44.0 h1:OlYfcVviAnwNN40QZUrrzU0QZjq3En7rCU5X09a/B7I= github.com/mark3labs/mcp-go v0.42.0 h1:gk/8nYJh8t3yroCAOBhNbYsM9TCKvkM13I5t5Hfu6Ls=
github.com/mark3labs/mcp-go v0.44.0/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw= github.com/mark3labs/mcp-go v0.42.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.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.uber.org/zap v1.27.0/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.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
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.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.37.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.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
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

@@ -5,7 +5,9 @@ import (
"gitea.com/gitea/gitea-mcp/pkg/flag" "gitea.com/gitea/gitea-mcp/pkg/flag"
) )
var Version = "dev" var (
Version = "dev"
)
func init() { func init() {
flag.Version = Version flag.Version = Version

View File

@@ -6,3 +6,5 @@ import (
// 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,14 +4,12 @@ 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" "gitea.com/gitea/gitea-mcp/pkg/gitea"
"gitea.com/gitea/gitea-mcp/pkg/log" "gitea.com/gitea/gitea-mcp/pkg/log"
"gitea.com/gitea/gitea-mcp/pkg/params"
"gitea.com/gitea/gitea-mcp/pkg/to" "gitea.com/gitea/gitea-mcp/pkg/to"
"github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/mcp"
@@ -69,7 +67,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 == http.StatusNotFound || httpErr.StatusCode == http.StatusMethodNotAllowed) { if errors.As(err, &httpErr) && (httpErr.StatusCode == 404 || httpErr.StatusCode == 405) {
continue continue
} }
return nil, p, err return nil, p, err
@@ -111,18 +109,28 @@ 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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok || repo == "" { if !ok || repo == "" {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
jobID, err := params.GetIndex(req.GetArguments(), "job_id") jobIDFloat, ok := req.GetArguments()["job_id"].(float64)
if err != nil || jobID <= 0 { if !ok || jobIDFloat <= 0 {
return to.ErrorResult(errors.New("job_id is required")) return to.ErrorResult(fmt.Errorf("job_id is required"))
} }
tailLines := int(params.GetOptionalInt(req.GetArguments(), "tail_lines", 200)) tailLinesFloat, _ := req.GetArguments()["tail_lines"].(float64)
maxBytes := int(params.GetOptionalInt(req.GetArguments(), "max_bytes", 65536)) maxBytesFloat, _ := req.GetArguments()["max_bytes"].(float64)
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))
@@ -132,13 +140,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),
}) })
} }
@@ -146,17 +154,18 @@ 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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok || repo == "" { if !ok || repo == "" {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
jobID, err := params.GetIndex(req.GetArguments(), "job_id") jobIDFloat, ok := req.GetArguments()["job_id"].(float64)
if err != nil || jobID <= 0 { if !ok || jobIDFloat <= 0 {
return to.ErrorResult(errors.New("job_id is required")) return to.ErrorResult(fmt.Errorf("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 {
@@ -185,3 +194,5 @@ func DownloadRepoActionJobLogFn(ctx context.Context, req mcp.CallToolRequest) (*
"bytes": len(raw), "bytes": len(raw),
}) })
} }

View File

@@ -20,3 +20,5 @@ 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,14 +4,10 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"maps"
"net/http"
"net/url" "net/url"
"strconv"
"gitea.com/gitea/gitea-mcp/pkg/gitea" "gitea.com/gitea/gitea-mcp/pkg/gitea"
"gitea.com/gitea/gitea-mcp/pkg/log" "gitea.com/gitea/gitea-mcp/pkg/log"
"gitea.com/gitea/gitea-mcp/pkg/params"
"gitea.com/gitea/gitea-mcp/pkg/to" "gitea.com/gitea/gitea-mcp/pkg/to"
"github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/mcp"
@@ -129,41 +125,47 @@ 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, respOut any) error { func doJSONWithFallback(ctx context.Context, method string, paths []string, query url.Values, body any, respOut any) (string, int, error) {
var lastErr error var lastErr error
for _, p := range paths { for _, p := range paths {
_, err := gitea.DoJSON(ctx, method, p, query, body, respOut) status, err := gitea.DoJSON(ctx, method, p, query, body, respOut)
if err == nil { if err == nil {
return nil return p, status, nil
} }
lastErr = err lastErr = err
var httpErr *gitea.HTTPError var httpErr *gitea.HTTPError
if errors.As(err, &httpErr) && (httpErr.StatusCode == http.StatusNotFound || httpErr.StatusCode == http.StatusMethodNotAllowed) { if errors.As(err, &httpErr) && (httpErr.StatusCode == 404 || httpErr.StatusCode == 405) {
continue continue
} }
return err return p, status, err
} }
return lastErr return "", 0, 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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok || repo == "" { if !ok || repo == "" {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("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", strconv.Itoa(int(page))) query.Set("page", fmt.Sprintf("%d", int(page)))
query.Set("limit", strconv.Itoa(int(pageSize))) query.Set("limit", fmt.Sprintf("%d", 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)),
}, },
@@ -179,19 +181,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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok || repo == "" { if !ok || repo == "" {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("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(errors.New("workflow_id is required")) return to.ErrorResult(fmt.Errorf("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)),
}, },
@@ -207,28 +209,30 @@ 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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok || repo == "" { if !ok || repo == "" {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("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(errors.New("workflow_id is required")) return to.ErrorResult(fmt.Errorf("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(errors.New("ref is required")) return to.ErrorResult(fmt.Errorf("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]any); ok { } else if m, ok := raw.(map[string]interface{}); ok {
inputs = make(map[string]any, len(m)) inputs = make(map[string]any, len(m))
maps.Copy(inputs, m) for k, v := range m {
inputs[k] = v
}
} }
} }
@@ -239,7 +243,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)),
@@ -248,7 +252,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 == http.StatusNotFound || httpErr.StatusCode == http.StatusMethodNotAllowed) { if errors.As(err, &httpErr) && (httpErr.StatusCode == 404 || httpErr.StatusCode == 405) {
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))
@@ -260,25 +264,31 @@ 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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok || repo == "" { if !ok || repo == "" {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("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", strconv.Itoa(int(page))) query.Set("page", fmt.Sprintf("%d", int(page)))
query.Set("limit", strconv.Itoa(int(pageSize))) query.Set("limit", fmt.Sprintf("%d", 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)),
}, },
@@ -294,21 +304,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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok || repo == "" { if !ok || repo == "" {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
runID, err := params.GetIndex(req.GetArguments(), "run_id") runID, ok := req.GetArguments()["run_id"].(float64)
if err != nil || runID <= 0 { if !ok || runID <= 0 {
return to.ErrorResult(errors.New("run_id is required")) return to.ErrorResult(fmt.Errorf("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), runID), fmt.Sprintf("repos/%s/%s/actions/runs/%d", url.PathEscape(owner), url.PathEscape(repo), int64(runID)),
}, },
nil, nil, &result, nil, nil, &result,
) )
@@ -322,20 +332,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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok || repo == "" { if !ok || repo == "" {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
runID, err := params.GetIndex(req.GetArguments(), "run_id") runID, ok := req.GetArguments()["run_id"].(float64)
if err != nil || runID <= 0 { if !ok || runID <= 0 {
return to.ErrorResult(errors.New("run_id is required")) return to.ErrorResult(fmt.Errorf("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), runID), fmt.Sprintf("repos/%s/%s/actions/runs/%d/cancel", url.PathEscape(owner), url.PathEscape(repo), int64(runID)),
}, },
nil, nil, nil, nil, nil, nil,
) )
@@ -349,27 +359,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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok || repo == "" { if !ok || repo == "" {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
runID, err := params.GetIndex(req.GetArguments(), "run_id") runID, ok := req.GetArguments()["run_id"].(float64)
if err != nil || runID <= 0 { if !ok || runID <= 0 {
return to.ErrorResult(errors.New("run_id is required")) return to.ErrorResult(fmt.Errorf("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), runID), 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-failed-jobs", 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)),
}, },
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 == http.StatusNotFound || httpErr.StatusCode == http.StatusMethodNotAllowed) { if errors.As(err, &httpErr) && (httpErr.StatusCode == 404 || httpErr.StatusCode == 405) {
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))
@@ -381,25 +391,31 @@ 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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok || repo == "" { if !ok || repo == "" {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("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", strconv.Itoa(int(page))) query.Set("page", fmt.Sprintf("%d", int(page)))
query.Set("limit", strconv.Itoa(int(pageSize))) query.Set("limit", fmt.Sprintf("%d", 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)),
}, },
@@ -415,27 +431,33 @@ 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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok || repo == "" { if !ok || repo == "" {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
runID, err := params.GetIndex(req.GetArguments(), "run_id") runID, ok := req.GetArguments()["run_id"].(float64)
if err != nil || runID <= 0 { if !ok || runID <= 0 {
return to.ErrorResult(errors.New("run_id is required")) return to.ErrorResult(fmt.Errorf("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", strconv.Itoa(int(page))) query.Set("page", fmt.Sprintf("%d", int(page)))
query.Set("limit", strconv.Itoa(int(pageSize))) query.Set("limit", fmt.Sprintf("%d", 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), runID), fmt.Sprintf("repos/%s/%s/actions/runs/%d/jobs", url.PathEscape(owner), url.PathEscape(repo), int64(runID)),
}, },
query, nil, &result, query, nil, &result,
) )

View File

@@ -2,14 +2,12 @@ package actions
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"net/url" "net/url"
"time" "time"
"gitea.com/gitea/gitea-mcp/pkg/gitea" "gitea.com/gitea/gitea-mcp/pkg/gitea"
"gitea.com/gitea/gitea-mcp/pkg/log" "gitea.com/gitea/gitea-mcp/pkg/log"
"gitea.com/gitea/gitea-mcp/pkg/params"
"gitea.com/gitea/gitea-mcp/pkg/to" "gitea.com/gitea/gitea-mcp/pkg/to"
gitea_sdk "code.gitea.io/sdk/gitea" gitea_sdk "code.gitea.io/sdk/gitea"
@@ -29,7 +27,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,omitzero"` CreatedAt time.Time `json:"created_at,omitempty"`
} }
var ( var (
@@ -99,14 +97,20 @@ 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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok || repo == "" { if !ok || repo == "" {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("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 {
@@ -138,19 +142,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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok || repo == "" { if !ok || repo == "" {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
name, ok := req.GetArguments()["name"].(string) name, ok := req.GetArguments()["name"].(string)
if !ok || name == "" { if !ok || name == "" {
return to.ErrorResult(errors.New("name is required")) return to.ErrorResult(fmt.Errorf("name is required"))
} }
data, ok := req.GetArguments()["data"].(string) data, ok := req.GetArguments()["data"].(string)
if !ok || data == "" { if !ok || data == "" {
return to.ErrorResult(errors.New("data is required")) return to.ErrorResult(fmt.Errorf("data is required"))
} }
description, _ := req.GetArguments()["description"].(string) description, _ := req.GetArguments()["description"].(string)
@@ -173,15 +177,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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok || repo == "" { if !ok || repo == "" {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
secretName, ok := req.GetArguments()["secretName"].(string) secretName, ok := req.GetArguments()["secretName"].(string)
if !ok || secretName == "" { if !ok || secretName == "" {
return to.ErrorResult(errors.New("secretName is required")) return to.ErrorResult(fmt.Errorf("secretName is required"))
} }
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
@@ -199,10 +203,16 @@ 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(errors.New("org is required")) return to.ErrorResult(fmt.Errorf("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 {
@@ -234,15 +244,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(errors.New("org is required")) return to.ErrorResult(fmt.Errorf("org is required"))
} }
name, ok := req.GetArguments()["name"].(string) name, ok := req.GetArguments()["name"].(string)
if !ok || name == "" { if !ok || name == "" {
return to.ErrorResult(errors.New("name is required")) return to.ErrorResult(fmt.Errorf("name is required"))
} }
data, ok := req.GetArguments()["data"].(string) data, ok := req.GetArguments()["data"].(string)
if !ok || data == "" { if !ok || data == "" {
return to.ErrorResult(errors.New("data is required")) return to.ErrorResult(fmt.Errorf("data is required"))
} }
description, _ := req.GetArguments()["description"].(string) description, _ := req.GetArguments()["description"].(string)
@@ -265,11 +275,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(errors.New("org is required")) return to.ErrorResult(fmt.Errorf("org is required"))
} }
secretName, ok := req.GetArguments()["secretName"].(string) secretName, ok := req.GetArguments()["secretName"].(string)
if !ok || secretName == "" { if !ok || secretName == "" {
return to.ErrorResult(errors.New("secretName is required")) return to.ErrorResult(fmt.Errorf("secretName is required"))
} }
escapedOrg := url.PathEscape(org) escapedOrg := url.PathEscape(org)

View File

@@ -2,14 +2,11 @@ package actions
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"net/url" "net/url"
"strconv"
"gitea.com/gitea/gitea-mcp/pkg/gitea" "gitea.com/gitea/gitea-mcp/pkg/gitea"
"gitea.com/gitea/gitea-mcp/pkg/log" "gitea.com/gitea/gitea-mcp/pkg/log"
"gitea.com/gitea/gitea-mcp/pkg/params"
"gitea.com/gitea/gitea-mcp/pkg/to" "gitea.com/gitea/gitea-mcp/pkg/to"
gitea_sdk "code.gitea.io/sdk/gitea" gitea_sdk "code.gitea.io/sdk/gitea"
@@ -24,11 +21,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 (
@@ -134,18 +131,24 @@ 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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok || repo == "" { if !ok || repo == "" {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("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", strconv.Itoa(int(page))) query.Set("page", fmt.Sprintf("%d", int(page)))
query.Set("limit", strconv.Itoa(int(pageSize))) query.Set("limit", fmt.Sprintf("%d", 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)
@@ -159,15 +162,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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok || repo == "" { if !ok || repo == "" {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
name, ok := req.GetArguments()["name"].(string) name, ok := req.GetArguments()["name"].(string)
if !ok || name == "" { if !ok || name == "" {
return to.ErrorResult(errors.New("name is required")) return to.ErrorResult(fmt.Errorf("name is required"))
} }
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
@@ -185,19 +188,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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok || repo == "" { if !ok || repo == "" {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
name, ok := req.GetArguments()["name"].(string) name, ok := req.GetArguments()["name"].(string)
if !ok || name == "" { if !ok || name == "" {
return to.ErrorResult(errors.New("name is required")) return to.ErrorResult(fmt.Errorf("name is required"))
} }
value, ok := req.GetArguments()["value"].(string) value, ok := req.GetArguments()["value"].(string)
if !ok || value == "" { if !ok || value == "" {
return to.ErrorResult(errors.New("value is required")) return to.ErrorResult(fmt.Errorf("value is required"))
} }
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
@@ -215,19 +218,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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok || repo == "" { if !ok || repo == "" {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
name, ok := req.GetArguments()["name"].(string) name, ok := req.GetArguments()["name"].(string)
if !ok || name == "" { if !ok || name == "" {
return to.ErrorResult(errors.New("name is required")) return to.ErrorResult(fmt.Errorf("name is required"))
} }
value, ok := req.GetArguments()["value"].(string) value, ok := req.GetArguments()["value"].(string)
if !ok || value == "" { if !ok || value == "" {
return to.ErrorResult(errors.New("value is required")) return to.ErrorResult(fmt.Errorf("value is required"))
} }
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
@@ -245,15 +248,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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok || repo == "" { if !ok || repo == "" {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
name, ok := req.GetArguments()["name"].(string) name, ok := req.GetArguments()["name"].(string)
if !ok || name == "" { if !ok || name == "" {
return to.ErrorResult(errors.New("name is required")) return to.ErrorResult(fmt.Errorf("name is required"))
} }
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
@@ -271,10 +274,16 @@ 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(errors.New("org is required")) return to.ErrorResult(fmt.Errorf("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 {
@@ -293,11 +302,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(errors.New("org is required")) return to.ErrorResult(fmt.Errorf("org is required"))
} }
name, ok := req.GetArguments()["name"].(string) name, ok := req.GetArguments()["name"].(string)
if !ok || name == "" { if !ok || name == "" {
return to.ErrorResult(errors.New("name is required")) return to.ErrorResult(fmt.Errorf("name is required"))
} }
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
@@ -315,15 +324,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(errors.New("org is required")) return to.ErrorResult(fmt.Errorf("org is required"))
} }
name, ok := req.GetArguments()["name"].(string) name, ok := req.GetArguments()["name"].(string)
if !ok || name == "" { if !ok || name == "" {
return to.ErrorResult(errors.New("name is required")) return to.ErrorResult(fmt.Errorf("name is required"))
} }
value, ok := req.GetArguments()["value"].(string) value, ok := req.GetArguments()["value"].(string)
if !ok || value == "" { if !ok || value == "" {
return to.ErrorResult(errors.New("value is required")) return to.ErrorResult(fmt.Errorf("value is required"))
} }
description, _ := req.GetArguments()["description"].(string) description, _ := req.GetArguments()["description"].(string)
@@ -346,15 +355,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(errors.New("org is required")) return to.ErrorResult(fmt.Errorf("org is required"))
} }
name, ok := req.GetArguments()["name"].(string) name, ok := req.GetArguments()["name"].(string)
if !ok || name == "" { if !ok || name == "" {
return to.ErrorResult(errors.New("name is required")) return to.ErrorResult(fmt.Errorf("name is required"))
} }
value, ok := req.GetArguments()["value"].(string) value, ok := req.GetArguments()["value"].(string)
if !ok || value == "" { if !ok || value == "" {
return to.ErrorResult(errors.New("value is required")) return to.ErrorResult(fmt.Errorf("value is required"))
} }
description, _ := req.GetArguments()["description"].(string) description, _ := req.GetArguments()["description"].(string)
@@ -376,11 +385,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(errors.New("org is required")) return to.ErrorResult(fmt.Errorf("org is required"))
} }
name, ok := req.GetArguments()["name"].(string) name, ok := req.GetArguments()["name"].(string)
if !ok || name == "" { if !ok || name == "" {
return to.ErrorResult(errors.New("name is required")) return to.ErrorResult(fmt.Errorf("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)
@@ -389,3 +398,5 @@ 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"})
} }

View File

@@ -2,12 +2,12 @@ package issue
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"gitea.com/gitea/gitea-mcp/pkg/gitea" "gitea.com/gitea/gitea-mcp/pkg/gitea"
"gitea.com/gitea/gitea-mcp/pkg/log" "gitea.com/gitea/gitea-mcp/pkg/log"
"gitea.com/gitea/gitea-mcp/pkg/params" "gitea.com/gitea/gitea-mcp/pkg/params"
"gitea.com/gitea/gitea-mcp/pkg/ptr"
"gitea.com/gitea/gitea-mcp/pkg/to" "gitea.com/gitea/gitea-mcp/pkg/to"
"gitea.com/gitea/gitea-mcp/pkg/tool" "gitea.com/gitea/gitea-mcp/pkg/tool"
@@ -73,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]any{"type": "string"})), mcp.WithArray("assignees", mcp.Description("usernames to assign to this issue"), mcp.Items(map[string]interface{}{"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")),
) )
@@ -131,11 +131,11 @@ 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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
index, err := params.GetIndex(req.GetArguments(), "index") index, err := params.GetIndex(req.GetArguments(), "index")
if err != nil { if err != nil {
@@ -157,18 +157,24 @@ 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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
state, ok := req.GetArguments()["state"].(string) state, ok := req.GetArguments()["state"].(string)
if !ok { if !ok {
state = "all" state = "all"
} }
page := params.GetOptionalInt(req.GetArguments(), "page", 1) page, ok := req.GetArguments()["page"].(float64)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) if !ok {
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{
@@ -191,19 +197,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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
title, ok := req.GetArguments()["title"].(string) title, ok := req.GetArguments()["title"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("title is required")) return to.ErrorResult(fmt.Errorf("title is required"))
} }
body, ok := req.GetArguments()["body"].(string) body, ok := req.GetArguments()["body"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("body is required")) return to.ErrorResult(fmt.Errorf("body is required"))
} }
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
if err != nil { if err != nil {
@@ -224,11 +230,11 @@ 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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
index, err := params.GetIndex(req.GetArguments(), "index") index, err := params.GetIndex(req.GetArguments(), "index")
if err != nil { if err != nil {
@@ -236,7 +242,7 @@ func CreateIssueCommentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Ca
} }
body, ok := req.GetArguments()["body"].(string) body, ok := req.GetArguments()["body"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("body is required")) return to.ErrorResult(fmt.Errorf("body is required"))
} }
opt := gitea_sdk.CreateIssueCommentOption{ opt := gitea_sdk.CreateIssueCommentOption{
Body: body, Body: body,
@@ -257,11 +263,11 @@ 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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
index, err := params.GetIndex(req.GetArguments(), "index") index, err := params.GetIndex(req.GetArguments(), "index")
if err != nil { if err != nil {
@@ -276,11 +282,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 = new(body) opt.Body = ptr.To(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.([]any); ok { if assigneesSlice, ok := assigneesArg.([]interface{}); 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)
@@ -289,14 +295,13 @@ func EditIssueFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRes
} }
} }
opt.Assignees = assignees opt.Assignees = assignees
if val, exists := req.GetArguments()["milestone"]; exists { milestone, ok := req.GetArguments()["milestone"].(float64)
if milestone, ok := params.ToInt64(val); ok { if ok {
opt.Milestone = new(milestone) opt.Milestone = ptr.To(int64(milestone))
}
} }
state, ok := req.GetArguments()["state"].(string) state, ok := req.GetArguments()["state"].(string)
if ok { if ok {
opt.State = new(gitea_sdk.StateType(state)) opt.State = ptr.To(gitea_sdk.StateType(state))
} }
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
@@ -315,19 +320,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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
commentID, err := params.GetIndex(req.GetArguments(), "commentID") commentID, ok := req.GetArguments()["commentID"].(float64)
if err != nil { if !ok {
return to.ErrorResult(err) return to.ErrorResult(fmt.Errorf("comment ID is required"))
} }
body, ok := req.GetArguments()["body"].(string) body, ok := req.GetArguments()["body"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("body is required")) return to.ErrorResult(fmt.Errorf("body is required"))
} }
opt := gitea_sdk.EditIssueCommentOption{ opt := gitea_sdk.EditIssueCommentOption{
Body: body, Body: body,
@@ -336,9 +341,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, commentID, opt) issueComment, _, err := client.EditIssueComment(owner, repo, int64(commentID), opt)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("edit %v/%v/issues/comments/%v err: %v", owner, repo, commentID, err)) return to.ErrorResult(fmt.Errorf("edit %v/%v/issues/comments/%v err: %v", owner, repo, int64(commentID), err))
} }
return to.TextResult(issueComment) return to.TextResult(issueComment)
@@ -348,11 +353,11 @@ 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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
index, err := params.GetIndex(req.GetArguments(), "index") index, err := params.GetIndex(req.GetArguments(), "index")
if err != nil { if err != nil {

View File

@@ -2,12 +2,12 @@ package label
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"gitea.com/gitea/gitea-mcp/pkg/gitea" "gitea.com/gitea/gitea-mcp/pkg/gitea"
"gitea.com/gitea/gitea-mcp/pkg/log" "gitea.com/gitea/gitea-mcp/pkg/log"
"gitea.com/gitea/gitea-mcp/pkg/params" "gitea.com/gitea/gitea-mcp/pkg/params"
"gitea.com/gitea/gitea-mcp/pkg/ptr"
"gitea.com/gitea/gitea-mcp/pkg/to" "gitea.com/gitea/gitea-mcp/pkg/to"
"gitea.com/gitea/gitea-mcp/pkg/tool" "gitea.com/gitea/gitea-mcp/pkg/tool"
@@ -28,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 (
@@ -87,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]any{"type": "number"})), mcp.WithArray("labels", mcp.Required(), mcp.Description("array of label IDs to add"), mcp.Items(map[string]interface{}{"type": "number"})),
) )
ReplaceIssueLabelsTool = mcp.NewTool( ReplaceIssueLabelsTool = mcp.NewTool(
@@ -96,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]any{"type": "number"})), mcp.WithArray("labels", mcp.Required(), mcp.Description("array of label IDs to replace with"), mcp.Items(map[string]interface{}{"type": "number"})),
) )
ClearIssueLabelsTool = mcp.NewTool( ClearIssueLabelsTool = mcp.NewTool(
@@ -116,41 +116,42 @@ 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,36 +191,42 @@ 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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("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{
@@ -242,24 +249,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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
id, err := params.GetIndex(req.GetArguments(), "id") id, ok := req.GetArguments()["id"].(float64)
if err != nil { if !ok {
return to.ErrorResult(err) return to.ErrorResult(fmt.Errorf("label ID is required"))
} }
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, id) label, _, err := client.GetRepoLabel(owner, repo, int64(id))
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("get %v/%v/label/%v err: %v", owner, repo, id, err)) return to.ErrorResult(fmt.Errorf("get %v/%v/label/%v err: %v", owner, repo, int64(id), err))
} }
return to.TextResult(label) return to.TextResult(label)
} }
@@ -268,19 +275,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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
name, ok := req.GetArguments()["name"].(string) name, ok := req.GetArguments()["name"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("name is required")) return to.ErrorResult(fmt.Errorf("name is required"))
} }
color, ok := req.GetArguments()["color"].(string) color, ok := req.GetArguments()["color"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("color is required")) return to.ErrorResult(fmt.Errorf("color is required"))
} }
description, _ := req.GetArguments()["description"].(string) // Optional description, _ := req.GetArguments()["description"].(string) // Optional
@@ -305,35 +312,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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
id, err := params.GetIndex(req.GetArguments(), "id") id, ok := req.GetArguments()["id"].(float64)
if err != nil { if !ok {
return to.ErrorResult(err) return to.ErrorResult(fmt.Errorf("label ID is required"))
} }
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 = new(name) opt.Name = ptr.To(name)
} }
if color, ok := req.GetArguments()["color"].(string); ok { if color, ok := req.GetArguments()["color"].(string); ok {
opt.Color = new(color) opt.Color = ptr.To(color)
} }
if description, ok := req.GetArguments()["description"].(string); ok { if description, ok := req.GetArguments()["description"].(string); ok {
opt.Description = new(description) opt.Description = ptr.To(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, id, opt) label, _, err := client.EditLabel(owner, repo, int64(id), opt)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("edit %v/%v/label/%v err: %v", owner, repo, id, err)) return to.ErrorResult(fmt.Errorf("edit %v/%v/label/%v err: %v", owner, repo, int64(id), err))
} }
return to.TextResult(label) return to.TextResult(label)
} }
@@ -342,24 +349,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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
id, err := params.GetIndex(req.GetArguments(), "id") id, ok := req.GetArguments()["id"].(float64)
if err != nil { if !ok {
return to.ErrorResult(err) return to.ErrorResult(fmt.Errorf("label ID is required"))
} }
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, id) _, err = client.DeleteLabel(owner, repo, int64(id))
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("delete %v/%v/label/%v err: %v", owner, repo, id, err)) return to.ErrorResult(fmt.Errorf("delete %v/%v/label/%v err: %v", owner, repo, int64(id), err))
} }
return to.TextResult("Label deleted successfully") return to.TextResult("Label deleted successfully")
} }
@@ -368,26 +375,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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
index, err := params.GetIndex(req.GetArguments(), "index") index, err := params.GetIndex(req.GetArguments(), "index")
if err != nil { if err != nil {
return to.ErrorResult(err) return to.ErrorResult(err)
} }
labelsRaw, ok := req.GetArguments()["labels"].([]any) labelsRaw, ok := req.GetArguments()["labels"].([]interface{})
if !ok { if !ok {
return to.ErrorResult(errors.New("labels (array of IDs) is required")) return to.ErrorResult(fmt.Errorf("labels (array of IDs) is required"))
} }
var labels []int64 var labels []int64
for _, l := range labelsRaw { for _, l := range labelsRaw {
if labelID, ok := params.ToInt64(l); ok { if labelID, ok := l.(float64); ok {
labels = append(labels, labelID) labels = append(labels, int64(labelID))
} else { } else {
return to.ErrorResult(errors.New("invalid label ID in labels array")) return to.ErrorResult(fmt.Errorf("invalid label ID in labels array"))
} }
} }
@@ -410,26 +417,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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
index, err := params.GetIndex(req.GetArguments(), "index") index, err := params.GetIndex(req.GetArguments(), "index")
if err != nil { if err != nil {
return to.ErrorResult(err) return to.ErrorResult(err)
} }
labelsRaw, ok := req.GetArguments()["labels"].([]any) labelsRaw, ok := req.GetArguments()["labels"].([]interface{})
if !ok { if !ok {
return to.ErrorResult(errors.New("labels (array of IDs) is required")) return to.ErrorResult(fmt.Errorf("labels (array of IDs) is required"))
} }
var labels []int64 var labels []int64
for _, l := range labelsRaw { for _, l := range labelsRaw {
if labelID, ok := params.ToInt64(l); ok { if labelID, ok := l.(float64); ok {
labels = append(labels, labelID) labels = append(labels, int64(labelID))
} else { } else {
return to.ErrorResult(errors.New("invalid label ID in labels array")) return to.ErrorResult(fmt.Errorf("invalid label ID in labels array"))
} }
} }
@@ -452,11 +459,11 @@ 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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
index, err := params.GetIndex(req.GetArguments(), "index") index, err := params.GetIndex(req.GetArguments(), "index")
if err != nil { if err != nil {
@@ -478,147 +485,153 @@ 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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
index, err := params.GetIndex(req.GetArguments(), "index") index, err := params.GetIndex(req.GetArguments(), "index")
if err != nil { if err != nil {
return to.ErrorResult(err) return to.ErrorResult(err)
} }
labelID, err := params.GetIndex(req.GetArguments(), "label_id") labelID, ok := req.GetArguments()["label_id"].(float64)
if err != nil { if !ok {
return to.ErrorResult(err) return to.ErrorResult(fmt.Errorf("label ID is required"))
} }
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, index, labelID) _, err = client.DeleteIssueLabel(owner, repo, index, int64(labelID))
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("remove label %v from %v/%v/issue/%v err: %v", labelID, owner, repo, index, err)) return to.ErrorResult(fmt.Errorf("remove label %v from %v/%v/issue/%v err: %v", int64(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(errors.New("org is required")) return to.ErrorResult(fmt.Errorf("org is required"))
} }
page := params.GetOptionalInt(req.GetArguments(), "page", 1) page, ok := req.GetArguments()["page"].(float64)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) if !ok {
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(errors.New("org is required")) return to.ErrorResult(fmt.Errorf("org is required"))
} }
name, ok := req.GetArguments()["name"].(string) name, ok := req.GetArguments()["name"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("name is required")) return to.ErrorResult(fmt.Errorf("name is required"))
} }
color, ok := req.GetArguments()["color"].(string) color, ok := req.GetArguments()["color"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("color is required")) return to.ErrorResult(fmt.Errorf("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(errors.New("org is required")) return to.ErrorResult(fmt.Errorf("org is required"))
} }
id, err := params.GetIndex(req.GetArguments(), "id") id, ok := req.GetArguments()["id"].(float64)
if err != nil { if !ok {
return to.ErrorResult(err) return to.ErrorResult(fmt.Errorf("label ID is required"))
} }
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 = new(name) opt.Name = ptr.To(name)
} }
if color, ok := req.GetArguments()["color"].(string); ok { if color, ok := req.GetArguments()["color"].(string); ok {
opt.Color = new(color) opt.Color = ptr.To(color)
} }
if description, ok := req.GetArguments()["description"].(string); ok { if description, ok := req.GetArguments()["description"].(string); ok {
opt.Description = new(description) opt.Description = ptr.To(description)
} }
if exclusive, ok := req.GetArguments()["exclusive"].(bool); ok { if exclusive, ok := req.GetArguments()["exclusive"].(bool); ok {
opt.Exclusive = new(exclusive) opt.Exclusive = ptr.To(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, id, opt) label, _, err := client.EditOrgLabel(org, int64(id), opt)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("edit %v/labels/%v err: %v", org, id, err)) return to.ErrorResult(fmt.Errorf("edit %v/labels/%v err: %v", org, int64(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(errors.New("org is required")) return to.ErrorResult(fmt.Errorf("org is required"))
} }
id, err := params.GetIndex(req.GetArguments(), "id") id, ok := req.GetArguments()["id"].(float64)
if err != nil { if !ok {
return to.ErrorResult(err) return to.ErrorResult(fmt.Errorf("label ID is required"))
} }
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, id) _, err = client.DeleteOrgLabel(org, int64(id))
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("delete %v/labels/%v err: %v", org, id, err)) return to.ErrorResult(fmt.Errorf("delete %v/labels/%v err: %v", org, int64(id), err))
} }
return to.TextResult("Label deleted successfully") return to.TextResult("Label deleted successfully")
} }

View File

@@ -2,12 +2,11 @@ package milestone
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"gitea.com/gitea/gitea-mcp/pkg/gitea" "gitea.com/gitea/gitea-mcp/pkg/gitea"
"gitea.com/gitea/gitea-mcp/pkg/log" "gitea.com/gitea/gitea-mcp/pkg/log"
"gitea.com/gitea/gitea-mcp/pkg/params" "gitea.com/gitea/gitea-mcp/pkg/ptr"
"gitea.com/gitea/gitea-mcp/pkg/to" "gitea.com/gitea/gitea-mcp/pkg/to"
"gitea.com/gitea/gitea-mcp/pkg/tool" "gitea.com/gitea/gitea-mcp/pkg/tool"
@@ -104,23 +103,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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
id, err := params.GetIndex(req.GetArguments(), "id") id, ok := req.GetArguments()["id"].(float64)
if err != nil { if !ok {
return to.ErrorResult(err) return to.ErrorResult(fmt.Errorf("id is required"))
} }
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, id) milestone, _, err := client.GetMilestone(owner, repo, int64(id))
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("get %v/%v/milestone/%v err: %v", owner, repo, id, err)) return to.ErrorResult(fmt.Errorf("get %v/%v/milestone/%v err: %v", owner, repo, int64(id), err))
} }
return to.TextResult(milestone) return to.TextResult(milestone)
@@ -130,11 +129,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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
state, ok := req.GetArguments()["state"].(string) state, ok := req.GetArguments()["state"].(string)
if !ok { if !ok {
@@ -144,8 +143,14 @@ func ListMilestonesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo
if !ok { if !ok {
name = "" name = ""
} }
page := params.GetOptionalInt(req.GetArguments(), "page", 1) page, ok := req.GetArguments()["page"].(float64)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) if !ok {
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,
@@ -169,15 +174,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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
title, ok := req.GetArguments()["title"].(string) title, ok := req.GetArguments()["title"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("title is required")) return to.ErrorResult(fmt.Errorf("title is required"))
} }
opt := gitea_sdk.CreateMilestoneOption{ opt := gitea_sdk.CreateMilestoneOption{
@@ -205,15 +210,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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
id, err := params.GetIndex(req.GetArguments(), "id") id, ok := req.GetArguments()["id"].(float64)
if err != nil { if !ok {
return to.ErrorResult(err) return to.ErrorResult(fmt.Errorf("id is required"))
} }
opt := gitea_sdk.EditMilestoneOption{} opt := gitea_sdk.EditMilestoneOption{}
@@ -224,20 +229,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 = new(description) opt.Description = ptr.To(description)
} }
state, ok := req.GetArguments()["state"].(string) state, ok := req.GetArguments()["state"].(string)
if ok { if ok {
opt.State = new(gitea_sdk.StateType(state)) opt.State = ptr.To(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, id, opt) milestone, _, err := client.EditMilestone(owner, repo, int64(id), opt)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("edit %v/%v/milestone/%v err: %v", owner, repo, id, err)) return to.ErrorResult(fmt.Errorf("edit %v/%v/milestone/%v err: %v", owner, repo, int64(id), err))
} }
return to.TextResult(milestone) return to.TextResult(milestone)
@@ -247,23 +252,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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
id, err := params.GetIndex(req.GetArguments(), "id") id, ok := req.GetArguments()["id"].(float64)
if err != nil { if !ok {
return to.ErrorResult(err) return to.ErrorResult(fmt.Errorf("id is required"))
} }
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, id) _, err = client.DeleteMilestone(owner, repo, int64(id))
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("delete %v/%v/milestone/%v err: %v", owner, repo, id, err)) return to.ErrorResult(fmt.Errorf("delete %v/%v/milestone/%v err: %v", owner, repo, int64(id), err))
} }
return to.TextResult("Milestone deleted successfully") return to.TextResult("Milestone deleted successfully")

View File

@@ -10,14 +10,14 @@ import (
"syscall" "syscall"
"time" "time"
"gitea.com/gitea/gitea-mcp/operation/actions"
"gitea.com/gitea/gitea-mcp/operation/issue" "gitea.com/gitea/gitea-mcp/operation/issue"
"gitea.com/gitea/gitea-mcp/operation/label" "gitea.com/gitea/gitea-mcp/operation/label"
"gitea.com/gitea/gitea-mcp/operation/milestone" "gitea.com/gitea/gitea-mcp/operation/milestone"
"gitea.com/gitea/gitea-mcp/operation/timetracking"
"gitea.com/gitea/gitea-mcp/operation/actions"
"gitea.com/gitea/gitea-mcp/operation/pull" "gitea.com/gitea/gitea-mcp/operation/pull"
"gitea.com/gitea/gitea-mcp/operation/repo" "gitea.com/gitea/gitea-mcp/operation/repo"
"gitea.com/gitea/gitea-mcp/operation/search" "gitea.com/gitea/gitea-mcp/operation/search"
"gitea.com/gitea/gitea-mcp/operation/timetracking"
"gitea.com/gitea/gitea-mcp/operation/user" "gitea.com/gitea/gitea-mcp/operation/user"
"gitea.com/gitea/gitea-mcp/operation/version" "gitea.com/gitea/gitea-mcp/operation/version"
"gitea.com/gitea/gitea-mcp/operation/wiki" "gitea.com/gitea/gitea-mcp/operation/wiki"
@@ -67,24 +67,20 @@ func RegisterTool(s *server.MCPServer) {
s.DeleteTools("") s.DeleteTools("")
} }
// parseAuthToken extracts the token from an Authorization header. // parseBearerToken extracts the Bearer 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 parseAuthToken(authHeader string) (string, bool) { func parseBearerToken(authHeader string) (string, bool) {
if len(authHeader) > 7 && strings.EqualFold(authHeader[:7], "Bearer ") { const bearerPrefix = "Bearer "
token := strings.TrimSpace(authHeader[7:]) if len(authHeader) < len(bearerPrefix) || !strings.HasPrefix(authHeader, bearerPrefix) {
if token != "" { return "", false
return token, true
}
} }
if len(authHeader) > 6 && strings.EqualFold(authHeader[:6], "token ") {
token := strings.TrimSpace(authHeader[6:]) token := strings.TrimSpace(authHeader[len(bearerPrefix):])
if token != "" { if token == "" {
return token, true return "", false
}
} }
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 {
@@ -93,7 +89,7 @@ func getContextWithToken(ctx context.Context, r *http.Request) context.Context {
return ctx return ctx
} }
token, ok := parseAuthToken(authHeader) token, ok := parseBearerToken(authHeader)
if !ok { if !ok {
return ctx return ctx
} }

View File

@@ -4,7 +4,7 @@ import (
"testing" "testing"
) )
func TestParseAuthToken(t *testing.T) { func TestParseBearerToken(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
header string header string
@@ -12,29 +12,23 @@ func TestParseAuthToken(t *testing.T) {
wantOK bool wantOK bool
}{ }{
{ {
name: "valid Bearer token", name: "valid 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 ",
@@ -53,24 +47,6 @@ func TestParseAuthToken(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",
@@ -84,7 +60,7 @@ func TestParseAuthToken(t *testing.T) {
wantOK: false, wantOK: false,
}, },
{ {
name: "bearer token with internal spaces", name: "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,
@@ -93,12 +69,12 @@ func TestParseAuthToken(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 := parseAuthToken(tt.header) gotToken, gotOK := parseBearerToken(tt.header)
if gotToken != tt.wantToken { if gotToken != tt.wantToken {
t.Errorf("parseAuthToken() token = %q, want %q", gotToken, tt.wantToken) t.Errorf("parseBearerToken() token = %q, want %q", gotToken, tt.wantToken)
} }
if gotOK != tt.wantOK { if gotOK != tt.wantOK {
t.Errorf("parseAuthToken() ok = %v, want %v", gotOK, tt.wantOK) t.Errorf("parseBearerToken() ok = %v, want %v", gotOK, tt.wantOK)
} }
}) })
} }

View File

@@ -2,7 +2,6 @@ package pull
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"gitea.com/gitea/gitea-mcp/pkg/gitea" "gitea.com/gitea/gitea-mcp/pkg/gitea"
@@ -32,8 +31,6 @@ 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 (
@@ -83,8 +80,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]any{"type": "string"})), mcp.WithArray("reviewers", mcp.Description("list of reviewer usernames"), 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"})), mcp.WithArray("team_reviewers", mcp.Description("list of team reviewer names"), mcp.Items(map[string]interface{}{"type": "string"})),
) )
DeletePullRequestReviewerTool = mcp.NewTool( DeletePullRequestReviewerTool = mcp.NewTool(
@@ -93,8 +90,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]any{"type": "string"})), mcp.WithArray("reviewers", mcp.Description("list of reviewer usernames 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"})), mcp.WithArray("team_reviewers", mcp.Description("list of team reviewer names to remove"), mcp.Items(map[string]interface{}{"type": "string"})),
) )
ListPullRequestReviewsTool = mcp.NewTool( ListPullRequestReviewsTool = mcp.NewTool(
@@ -134,13 +131,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]any{ mcp.WithArray("comments", mcp.Description("inline review comments (objects with path, body, old_line_num, new_line_num)"), mcp.Items(map[string]interface{}{
"type": "object", "type": "object",
"properties": map[string]any{ "properties": map[string]interface{}{
"path": map[string]any{"type": "string", "description": "file path to comment on"}, "path": map[string]interface{}{"type": "string", "description": "file path to comment on"},
"body": map[string]any{"type": "string", "description": "comment body"}, "body": map[string]interface{}{"type": "string", "description": "comment body"},
"old_line_num": map[string]any{"type": "number", "description": "line number in the old file (for deletions/changes)"}, "old_line_num": map[string]interface{}{"type": "number", "description": "line number in the old file (for deletions/changes)"},
"new_line_num": map[string]any{"type": "number", "description": "line number in the new file (for additions/changes)"}, "new_line_num": map[string]interface{}{"type": "number", "description": "line number in the new file (for additions/changes)"},
}, },
})), })),
) )
@@ -174,34 +171,6 @@ 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() {
@@ -257,25 +226,17 @@ 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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
index, err := params.GetIndex(req.GetArguments(), "index") index, err := params.GetIndex(req.GetArguments(), "index")
if err != nil { if err != nil {
@@ -297,11 +258,11 @@ func GetPullRequestDiffFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Ca
log.Debugf("Called GetPullRequestDiffFn") log.Debugf("Called GetPullRequestDiffFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
index, err := params.GetIndex(req.GetArguments(), "index") index, err := params.GetIndex(req.GetArguments(), "index")
if err != nil { if err != nil {
@@ -320,7 +281,7 @@ func GetPullRequestDiffFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Ca
return to.ErrorResult(fmt.Errorf("get %v/%v/pr/%v diff err: %v", owner, repo, index, err)) return to.ErrorResult(fmt.Errorf("get %v/%v/pr/%v diff err: %v", owner, repo, index, err))
} }
result := map[string]any{ result := map[string]interface{}{
"diff": string(diffBytes), "diff": string(diffBytes),
"binary": binary, "binary": binary,
"index": index, "index": index,
@@ -334,24 +295,30 @@ func ListRepoPullRequestsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.
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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("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 := params.GetOptionalInt(req.GetArguments(), "milestone", 0) milestone, _ := req.GetArguments()["milestone"].(float64)
page := params.GetOptionalInt(req.GetArguments(), "page", 1) page, ok := req.GetArguments()["page"].(float64)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) if !ok {
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: milestone, Milestone: int64(milestone),
ListOptions: gitea_sdk.ListOptions{ ListOptions: gitea_sdk.ListOptions{
Page: int(page), Page: int(page),
PageSize: int(pageSize), PageSize: int(pageSize),
@@ -373,27 +340,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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
title, ok := req.GetArguments()["title"].(string) title, ok := req.GetArguments()["title"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("title is required")) return to.ErrorResult(fmt.Errorf("title is required"))
} }
body, ok := req.GetArguments()["body"].(string) body, ok := req.GetArguments()["body"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("body is required")) return to.ErrorResult(fmt.Errorf("body is required"))
} }
head, ok := req.GetArguments()["head"].(string) head, ok := req.GetArguments()["head"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("head is required")) return to.ErrorResult(fmt.Errorf("head is required"))
} }
base, ok := req.GetArguments()["base"].(string) base, ok := req.GetArguments()["base"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("base is required")) return to.ErrorResult(fmt.Errorf("base is required"))
} }
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
if err != nil { if err != nil {
@@ -416,11 +383,11 @@ 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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
index, err := params.GetIndex(req.GetArguments(), "index") index, err := params.GetIndex(req.GetArguments(), "index")
if err != nil { if err != nil {
@@ -429,7 +396,7 @@ func CreatePullRequestReviewerFn(ctx context.Context, req mcp.CallToolRequest) (
var reviewers []string var reviewers []string
if reviewersArg, exists := req.GetArguments()["reviewers"]; exists { if reviewersArg, exists := req.GetArguments()["reviewers"]; exists {
if reviewersSlice, ok := reviewersArg.([]any); ok { if reviewersSlice, ok := reviewersArg.([]interface{}); 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)
@@ -440,7 +407,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.([]any); ok { if teamReviewersSlice, ok := teamReviewersArg.([]interface{}); 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)
@@ -463,7 +430,7 @@ func CreatePullRequestReviewerFn(ctx context.Context, req mcp.CallToolRequest) (
} }
// 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]any{ successMsg := map[string]interface{}{
"message": "Successfully created review requests", "message": "Successfully created review requests",
"reviewers": reviewers, "reviewers": reviewers,
"team_reviewers": teamReviewers, "team_reviewers": teamReviewers,
@@ -478,11 +445,11 @@ 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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
index, err := params.GetIndex(req.GetArguments(), "index") index, err := params.GetIndex(req.GetArguments(), "index")
if err != nil { if err != nil {
@@ -491,7 +458,7 @@ func DeletePullRequestReviewerFn(ctx context.Context, req mcp.CallToolRequest) (
var reviewers []string var reviewers []string
if reviewersArg, exists := req.GetArguments()["reviewers"]; exists { if reviewersArg, exists := req.GetArguments()["reviewers"]; exists {
if reviewersSlice, ok := reviewersArg.([]any); ok { if reviewersSlice, ok := reviewersArg.([]interface{}); 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)
@@ -502,7 +469,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.([]any); ok { if teamReviewersSlice, ok := teamReviewersArg.([]interface{}); 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)
@@ -524,7 +491,7 @@ func DeletePullRequestReviewerFn(ctx context.Context, req mcp.CallToolRequest) (
return to.ErrorResult(fmt.Errorf("delete review requests for %v/%v/pr/%v err: %v", owner, repo, index, err)) return to.ErrorResult(fmt.Errorf("delete review requests for %v/%v/pr/%v err: %v", owner, repo, index, err))
} }
successMsg := map[string]any{ successMsg := map[string]interface{}{
"message": "Successfully deleted review requests", "message": "Successfully deleted review requests",
"reviewers": reviewers, "reviewers": reviewers,
"team_reviewers": teamReviewers, "team_reviewers": teamReviewers,
@@ -539,18 +506,24 @@ 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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
index, err := params.GetIndex(req.GetArguments(), "index") index, err := params.GetIndex(req.GetArguments(), "index")
if err != nil { if err != nil {
return to.ErrorResult(err) return to.ErrorResult(err)
} }
page := params.GetOptionalInt(req.GetArguments(), "page", 1) page, ok := req.GetArguments()["page"].(float64)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) if !ok {
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 {
@@ -574,19 +547,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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
index, err := params.GetIndex(req.GetArguments(), "index") index, err := params.GetIndex(req.GetArguments(), "index")
if err != nil { if err != nil {
return to.ErrorResult(err) return to.ErrorResult(err)
} }
reviewID, err := params.GetIndex(req.GetArguments(), "review_id") reviewID, ok := req.GetArguments()["review_id"].(float64)
if err != nil { if !ok {
return to.ErrorResult(err) return to.ErrorResult(fmt.Errorf("review_id is required"))
} }
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
@@ -594,9 +567,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, index, reviewID) review, _, err := client.GetPullReview(owner, repo, index, int64(reviewID))
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("get review %v for %v/%v/pr/%v err: %v", reviewID, owner, repo, index, err)) return to.ErrorResult(fmt.Errorf("get review %v for %v/%v/pr/%v err: %v", int64(reviewID), owner, repo, index, err))
} }
return to.TextResult(review) return to.TextResult(review)
@@ -606,19 +579,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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
index, err := params.GetIndex(req.GetArguments(), "index") index, err := params.GetIndex(req.GetArguments(), "index")
if err != nil { if err != nil {
return to.ErrorResult(err) return to.ErrorResult(err)
} }
reviewID, err := params.GetIndex(req.GetArguments(), "review_id") reviewID, ok := req.GetArguments()["review_id"].(float64)
if err != nil { if !ok {
return to.ErrorResult(err) return to.ErrorResult(fmt.Errorf("review_id is required"))
} }
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
@@ -626,9 +599,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, index, reviewID) comments, _, err := client.ListPullReviewComments(owner, repo, index, int64(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", reviewID, owner, repo, index, err)) return to.ErrorResult(fmt.Errorf("list review comments for review %v on %v/%v/pr/%v err: %v", int64(reviewID), owner, repo, index, err))
} }
return to.TextResult(comments) return to.TextResult(comments)
@@ -638,11 +611,11 @@ 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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
index, err := params.GetIndex(req.GetArguments(), "index") index, err := params.GetIndex(req.GetArguments(), "index")
if err != nil { if err != nil {
@@ -663,9 +636,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.([]any); ok { if commentsSlice, ok := commentsArg.([]interface{}); ok {
for _, comment := range commentsSlice { for _, comment := range commentsSlice {
if commentMap, ok := comment.(map[string]any); ok { if commentMap, ok := comment.(map[string]interface{}); 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
@@ -673,11 +646,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 := params.ToInt64(commentMap["old_line_num"]); ok { if oldLineNum, ok := commentMap["old_line_num"].(float64); ok {
reviewComment.OldLineNum = oldLineNum reviewComment.OldLineNum = int64(oldLineNum)
} }
if newLineNum, ok := params.ToInt64(commentMap["new_line_num"]); ok { if newLineNum, ok := commentMap["new_line_num"].(float64); ok {
reviewComment.NewLineNum = newLineNum reviewComment.NewLineNum = int64(newLineNum)
} }
opt.Comments = append(opt.Comments, reviewComment) opt.Comments = append(opt.Comments, reviewComment)
} }
@@ -702,23 +675,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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
index, err := params.GetIndex(req.GetArguments(), "index") index, err := params.GetIndex(req.GetArguments(), "index")
if err != nil { if err != nil {
return to.ErrorResult(err) return to.ErrorResult(err)
} }
reviewID, err := params.GetIndex(req.GetArguments(), "review_id") reviewID, ok := req.GetArguments()["review_id"].(float64)
if err != nil { if !ok {
return to.ErrorResult(err) return to.ErrorResult(fmt.Errorf("review_id is required"))
} }
state, ok := req.GetArguments()["state"].(string) state, ok := req.GetArguments()["state"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("state is required")) return to.ErrorResult(fmt.Errorf("state is required"))
} }
opt := gitea_sdk.SubmitPullReviewOptions{ opt := gitea_sdk.SubmitPullReviewOptions{
@@ -733,9 +706,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, index, reviewID, opt) review, _, err := client.SubmitPullReview(owner, repo, index, int64(reviewID), opt)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("submit review %v for %v/%v/pr/%v err: %v", reviewID, owner, repo, index, err)) return to.ErrorResult(fmt.Errorf("submit review %v for %v/%v/pr/%v err: %v", int64(reviewID), owner, repo, index, err))
} }
return to.TextResult(review) return to.TextResult(review)
@@ -745,19 +718,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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
index, err := params.GetIndex(req.GetArguments(), "index") index, err := params.GetIndex(req.GetArguments(), "index")
if err != nil { if err != nil {
return to.ErrorResult(err) return to.ErrorResult(err)
} }
reviewID, err := params.GetIndex(req.GetArguments(), "review_id") reviewID, ok := req.GetArguments()["review_id"].(float64)
if err != nil { if !ok {
return to.ErrorResult(err) return to.ErrorResult(fmt.Errorf("review_id is required"))
} }
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
@@ -765,14 +738,14 @@ 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, index, reviewID) _, err = client.DeletePullReview(owner, repo, index, int64(reviewID))
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("delete review %v for %v/%v/pr/%v err: %v", reviewID, owner, repo, index, err)) return to.ErrorResult(fmt.Errorf("delete review %v for %v/%v/pr/%v err: %v", int64(reviewID), owner, repo, index, err))
} }
successMsg := map[string]any{ successMsg := map[string]interface{}{
"message": "Successfully deleted review", "message": "Successfully deleted review",
"review_id": reviewID, "review_id": int64(reviewID),
"pr_index": index, "pr_index": index,
"repository": fmt.Sprintf("%s/%s", owner, repo), "repository": fmt.Sprintf("%s/%s", owner, repo),
} }
@@ -784,19 +757,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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
index, err := params.GetIndex(req.GetArguments(), "index") index, err := params.GetIndex(req.GetArguments(), "index")
if err != nil { if err != nil {
return to.ErrorResult(err) return to.ErrorResult(err)
} }
reviewID, err := params.GetIndex(req.GetArguments(), "review_id") reviewID, ok := req.GetArguments()["review_id"].(float64)
if err != nil { if !ok {
return to.ErrorResult(err) return to.ErrorResult(fmt.Errorf("review_id is required"))
} }
opt := gitea_sdk.DismissPullReviewOptions{} opt := gitea_sdk.DismissPullReviewOptions{}
@@ -809,153 +782,17 @@ 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, index, reviewID, opt) _, err = client.DismissPullReview(owner, repo, index, int64(reviewID), opt)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("dismiss review %v for %v/%v/pr/%v err: %v", reviewID, owner, repo, index, err)) return to.ErrorResult(fmt.Errorf("dismiss review %v for %v/%v/pr/%v err: %v", int64(reviewID), owner, repo, index, err))
} }
successMsg := map[string]any{ successMsg := map[string]interface{}{
"message": "Successfully dismissed review", "message": "Successfully dismissed review",
"review_id": reviewID, "review_id": int64(reviewID),
"pr_index": 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)
}

View File

@@ -13,251 +13,6 @@ import (
"github.com/mark3labs/mcp-go/mcp" "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) { func TestGetPullRequestDiffFn(t *testing.T) {
const ( const (
owner = "octo" owner = "octo"
@@ -266,132 +21,120 @@ func TestGetPullRequestDiffFn(t *testing.T) {
diffRaw = "diff --git a/file.txt b/file.txt\n+line\n" diffRaw = "diff --git a/file.txt b/file.txt\n+line\n"
) )
indexInputs := []struct { var (
name string mu sync.Mutex
val any diffRequested bool
}{ binaryValue string
{"float64", float64(index)}, )
{"string", "12"}, errCh := make(chan error, 1)
}
for _, ii := range indexInputs { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Run(ii.name, func(t *testing.T) { switch r.URL.Path {
var ( case "/api/v1/version":
mu sync.Mutex w.Header().Set("Content-Type", "application/json")
diffRequested bool _, _ = w.Write([]byte(`{"version":"1.12.0"}`))
binaryValue string case fmt.Sprintf("/api/v1/repos/%s/%s", owner, repo):
) w.Header().Set("Content-Type", "application/json")
errCh := make(chan error, 1) _, _ = w.Write([]byte(`{"private":false}`))
case fmt.Sprintf("/%s/%s/pulls/%d.diff", owner, repo, index):
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet {
switch r.URL.Path { select {
case "/api/v1/version": case errCh <- fmt.Errorf("unexpected method: %s", r.Method):
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: 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,
},
},
} }
mu.Lock()
result, err := GetPullRequestDiffFn(context.Background(), req) diffRequested = true
if err != nil { binaryValue = r.URL.Query().Get("binary")
t.Fatalf("GetPullRequestDiffFn() error = %v", err) mu.Unlock()
} w.Header().Set("Content-Type", "text/plain")
_, _ = w.Write([]byte(diffRaw))
default:
select { select {
case reqErr := <-errCh: case errCh <- fmt.Errorf("unexpected request path: %s", r.URL.Path):
t.Fatalf("handler error: %v", reqErr)
default: default:
} }
}
})
mu.Lock() server := httptest.NewServer(handler)
requested := diffRequested defer server.Close()
gotBinary := binaryValue
mu.Unlock()
if !requested { origHost := flag.Host
t.Fatalf("expected diff request to be made") origToken := flag.Token
} origVersion := flag.Version
if gotBinary != "true" { flag.Host = server.URL
t.Fatalf("expected binary=true query param, got %q", gotBinary) flag.Token = ""
} flag.Version = "test"
defer func() {
flag.Host = origHost
flag.Token = origToken
flag.Version = origVersion
}()
if len(result.Content) == 0 { req := mcp.CallToolRequest{
t.Fatalf("expected content in result") Params: mcp.CallToolParams{
} Arguments: map[string]any{
"owner": owner,
"repo": repo,
"index": float64(index),
"binary": true,
},
},
}
textContent, ok := mcp.AsTextContent(result.Content[0]) result, err := GetPullRequestDiffFn(context.Background(), req)
if !ok { if err != nil {
t.Fatalf("expected text content, got %T", result.Content[0]) t.Fatalf("GetPullRequestDiffFn() error = %v", err)
} }
var parsed struct { select {
Result map[string]any `json:"Result"` case reqErr := <-errCh:
} t.Fatalf("handler error: %v", reqErr)
if err := json.Unmarshal([]byte(textContent.Text), &parsed); err != nil { default:
t.Fatalf("unmarshal result text: %v", err) }
}
if got, ok := parsed.Result["diff"].(string); !ok || got != diffRaw { mu.Lock()
t.Fatalf("diff = %q, want %q", got, diffRaw) requested := diffRequested
} gotBinary := binaryValue
if got, ok := parsed.Result["binary"].(bool); !ok || got != true { mu.Unlock()
t.Fatalf("binary = %v, want true", got)
} if !requested {
if got, ok := parsed.Result["index"].(float64); !ok || int64(got) != int64(index) { t.Fatalf("expected diff request to be made")
t.Fatalf("index = %v, want %d", got, index) }
} if gotBinary != "true" {
if got, ok := parsed.Result["owner"].(string); !ok || got != owner { t.Fatalf("expected binary=true query param, got %q", gotBinary)
t.Fatalf("owner = %q, want %q", got, owner) }
}
if got, ok := parsed.Result["repo"].(string); !ok || got != repo { if len(result.Content) == 0 {
t.Fatalf("repo = %q, want %q", got, repo) 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,7 +2,6 @@ package repo
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"gitea.com/gitea/gitea-mcp/pkg/gitea" "gitea.com/gitea/gitea-mcp/pkg/gitea"
@@ -65,15 +64,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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
branch, ok := req.GetArguments()["branch"].(string) branch, ok := req.GetArguments()["branch"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("branch is required")) return to.ErrorResult(fmt.Errorf("branch is required"))
} }
oldBranch, _ := req.GetArguments()["old_branch"].(string) oldBranch, _ := req.GetArguments()["old_branch"].(string)
@@ -96,15 +95,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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
branch, ok := req.GetArguments()["branch"].(string) branch, ok := req.GetArguments()["branch"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("branch is required")) return to.ErrorResult(fmt.Errorf("branch is required"))
} }
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
if err != nil { if err != nil {
@@ -122,11 +121,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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
opt := gitea_sdk.ListRepoBranchesOptions{ opt := gitea_sdk.ListRepoBranchesOptions{
ListOptions: gitea_sdk.ListOptions{ ListOptions: gitea_sdk.ListOptions{

View File

@@ -2,12 +2,10 @@ package repo
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"gitea.com/gitea/gitea-mcp/pkg/gitea" "gitea.com/gitea/gitea-mcp/pkg/gitea"
"gitea.com/gitea/gitea-mcp/pkg/log" "gitea.com/gitea/gitea-mcp/pkg/log"
"gitea.com/gitea/gitea-mcp/pkg/params"
"gitea.com/gitea/gitea-mcp/pkg/to" "gitea.com/gitea/gitea-mcp/pkg/to"
gitea_sdk "code.gitea.io/sdk/gitea" gitea_sdk "code.gitea.io/sdk/gitea"
@@ -41,19 +39,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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
page, err := params.GetIndex(req.GetArguments(), "page") page, ok := req.GetArguments()["page"].(float64)
if err != nil { if !ok {
return to.ErrorResult(err) return to.ErrorResult(fmt.Errorf("page is required"))
} }
pageSize, err := params.GetIndex(req.GetArguments(), "page_size") pageSize, ok := req.GetArguments()["page_size"].(float64)
if err != nil { if !ok {
return to.ErrorResult(err) return to.ErrorResult(fmt.Errorf("page_size is required"))
} }
sha, _ := req.GetArguments()["sha"].(string) sha, _ := req.GetArguments()["sha"].(string)
path, _ := req.GetArguments()["path"].(string) path, _ := req.GetArguments()["path"].(string)

View File

@@ -6,7 +6,6 @@ import (
"context" "context"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"gitea.com/gitea/gitea-mcp/pkg/gitea" "gitea.com/gitea/gitea-mcp/pkg/gitea"
@@ -114,16 +113,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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("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(errors.New("filePath is required")) return to.ErrorResult(fmt.Errorf("filePath is required"))
} }
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
if err != nil { if err != nil {
@@ -152,6 +151,7 @@ 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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("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(errors.New("filePath is required")) return to.ErrorResult(fmt.Errorf("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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
filePath, ok := req.GetArguments()["filePath"].(string) filePath, ok := req.GetArguments()["filePath"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("filePath is required")) return to.ErrorResult(fmt.Errorf("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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
filePath, ok := req.GetArguments()["filePath"].(string) filePath, ok := req.GetArguments()["filePath"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("filePath is required")) return to.ErrorResult(fmt.Errorf("filePath is required"))
} }
sha, ok := req.GetArguments()["sha"].(string) sha, ok := req.GetArguments()["sha"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("sha is required")) return to.ErrorResult(fmt.Errorf("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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
filePath, ok := req.GetArguments()["filePath"].(string) filePath, ok := req.GetArguments()["filePath"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("filePath is required")) return to.ErrorResult(fmt.Errorf("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(errors.New("sha is required")) return to.ErrorResult(fmt.Errorf("sha is required"))
} }
opt := gitea_sdk.DeleteFileOptions{ opt := gitea_sdk.DeleteFileOptions{
FileOptions: gitea_sdk.FileOptions{ FileOptions: gitea_sdk.FileOptions{

View File

@@ -2,13 +2,12 @@ package repo
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"time" "time"
"gitea.com/gitea/gitea-mcp/pkg/gitea" "gitea.com/gitea/gitea-mcp/pkg/gitea"
"gitea.com/gitea/gitea-mcp/pkg/log" "gitea.com/gitea/gitea-mcp/pkg/log"
"gitea.com/gitea/gitea-mcp/pkg/params" "gitea.com/gitea/gitea-mcp/pkg/ptr"
"gitea.com/gitea/gitea-mcp/pkg/to" "gitea.com/gitea/gitea-mcp/pkg/to"
gitea_sdk "code.gitea.io/sdk/gitea" gitea_sdk "code.gitea.io/sdk/gitea"
@@ -113,23 +112,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, errors.New("owner is required") return nil, fmt.Errorf("owner is required")
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return nil, errors.New("repo is required") return nil, fmt.Errorf("repo is required")
} }
tagName, ok := req.GetArguments()["tag_name"].(string) tagName, ok := req.GetArguments()["tag_name"].(string)
if !ok { if !ok {
return nil, errors.New("tag_name is required") return nil, fmt.Errorf("tag_name is required")
} }
target, ok := req.GetArguments()["target"].(string) target, ok := req.GetArguments()["target"].(string)
if !ok { if !ok {
return nil, errors.New("target is required") return nil, fmt.Errorf("target is required")
} }
title, ok := req.GetArguments()["title"].(string) title, ok := req.GetArguments()["title"].(string)
if !ok { if !ok {
return nil, errors.New("title is required") return nil, fmt.Errorf("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)
@@ -158,22 +157,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, errors.New("owner is required") return nil, fmt.Errorf("owner is required")
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return nil, errors.New("repo is required") return nil, fmt.Errorf("repo is required")
} }
id, err := params.GetIndex(req.GetArguments(), "id") id, ok := req.GetArguments()["id"].(float64)
if err != nil { if !ok {
return to.ErrorResult(err) return nil, fmt.Errorf("id is required")
} }
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, id) _, err = client.DeleteRelease(owner, repo, int64(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)
} }
@@ -185,22 +184,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, errors.New("owner is required") return nil, fmt.Errorf("owner is required")
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return nil, errors.New("repo is required") return nil, fmt.Errorf("repo is required")
} }
id, err := params.GetIndex(req.GetArguments(), "id") id, ok := req.GetArguments()["id"].(float64)
if err != nil { if !ok {
return to.ErrorResult(err) return nil, fmt.Errorf("id is required")
} }
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, id) release, _, err := client.GetRelease(owner, repo, int64(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)
} }
@@ -212,11 +211,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, errors.New("owner is required") return nil, fmt.Errorf("owner is required")
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return nil, errors.New("repo is required") return nil, fmt.Errorf("repo is required")
} }
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
@@ -235,24 +234,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, errors.New("owner is required") return nil, fmt.Errorf("owner is required")
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return nil, errors.New("repo is required") return nil, fmt.Errorf("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 = new(isDraft) pIsDraft = ptr.To(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 = new(isPreRelease) pIsPreRelease = ptr.To(isPreRelease)
} }
page := params.GetOptionalInt(req.GetArguments(), "page", 1) page, _ := req.GetArguments()["page"].(float64)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 20) pageSize, _ := req.GetArguments()["pageSize"].(float64)
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
if err != nil { if err != nil {

View File

@@ -7,7 +7,7 @@ import (
"gitea.com/gitea/gitea-mcp/pkg/gitea" "gitea.com/gitea/gitea-mcp/pkg/gitea"
"gitea.com/gitea/gitea-mcp/pkg/log" "gitea.com/gitea/gitea-mcp/pkg/log"
"gitea.com/gitea/gitea-mcp/pkg/params" "gitea.com/gitea/gitea-mcp/pkg/ptr"
"gitea.com/gitea/gitea-mcp/pkg/to" "gitea.com/gitea/gitea-mcp/pkg/to"
"gitea.com/gitea/gitea-mcp/pkg/tool" "gitea.com/gitea/gitea-mcp/pkg/tool"
@@ -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 := new(organization) organizationPtr := ptr.To(organization)
if !ok || organization == "" { if !ok || organization == "" {
organizationPtr = nil organizationPtr = nil
} }
name, ok := req.GetArguments()["name"].(string) name, ok := req.GetArguments()["name"].(string)
namePtr := new(name) namePtr := ptr.To(name)
if !ok || name == "" { if !ok || name == "" {
namePtr = nil namePtr = nil
} }
@@ -192,8 +192,14 @@ 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 := params.GetOptionalInt(req.GetArguments(), "page", 1) page, ok := req.GetArguments()["page"].(float64)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) if !ok {
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

@@ -2,12 +2,10 @@ package repo
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"gitea.com/gitea/gitea-mcp/pkg/gitea" "gitea.com/gitea/gitea-mcp/pkg/gitea"
"gitea.com/gitea/gitea-mcp/pkg/log" "gitea.com/gitea/gitea-mcp/pkg/log"
"gitea.com/gitea/gitea-mcp/pkg/params"
"gitea.com/gitea/gitea-mcp/pkg/to" "gitea.com/gitea/gitea-mcp/pkg/to"
gitea_sdk "code.gitea.io/sdk/gitea" gitea_sdk "code.gitea.io/sdk/gitea"
@@ -91,15 +89,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, errors.New("owner is required") return nil, fmt.Errorf("owner is required")
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return nil, errors.New("repo is required") return nil, fmt.Errorf("repo is required")
} }
tagName, ok := req.GetArguments()["tag_name"].(string) tagName, ok := req.GetArguments()["tag_name"].(string)
if !ok { if !ok {
return nil, errors.New("tag_name is required") return nil, fmt.Errorf("tag_name is required")
} }
target, _ := req.GetArguments()["target"].(string) target, _ := req.GetArguments()["target"].(string)
message, _ := req.GetArguments()["message"].(string) message, _ := req.GetArguments()["message"].(string)
@@ -124,15 +122,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, errors.New("owner is required") return nil, fmt.Errorf("owner is required")
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return nil, errors.New("repo is required") return nil, fmt.Errorf("repo is required")
} }
tagName, ok := req.GetArguments()["tag_name"].(string) tagName, ok := req.GetArguments()["tag_name"].(string)
if !ok { if !ok {
return nil, errors.New("tag_name is required") return nil, fmt.Errorf("tag_name is required")
} }
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
@@ -151,15 +149,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, errors.New("owner is required") return nil, fmt.Errorf("owner is required")
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return nil, errors.New("repo is required") return nil, fmt.Errorf("repo is required")
} }
tagName, ok := req.GetArguments()["tag_name"].(string) tagName, ok := req.GetArguments()["tag_name"].(string)
if !ok { if !ok {
return nil, errors.New("tag_name is required") return nil, fmt.Errorf("tag_name is required")
} }
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
@@ -178,14 +176,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, errors.New("owner is required") return nil, fmt.Errorf("owner is required")
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return nil, errors.New("repo is required") return nil, fmt.Errorf("repo is required")
} }
page := params.GetOptionalInt(req.GetArguments(), "page", 1) page, _ := req.GetArguments()["page"].(float64)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 20) pageSize, _ := req.GetArguments()["pageSize"].(float64)
client, err := gitea.ClientFromContext(ctx) client, err := gitea.ClientFromContext(ctx)
if err != nil { if err != nil {

View File

@@ -2,12 +2,11 @@ package search
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"gitea.com/gitea/gitea-mcp/pkg/gitea" "gitea.com/gitea/gitea-mcp/pkg/gitea"
"gitea.com/gitea/gitea-mcp/pkg/log" "gitea.com/gitea/gitea-mcp/pkg/log"
"gitea.com/gitea/gitea-mcp/pkg/params" "gitea.com/gitea/gitea-mcp/pkg/ptr"
"gitea.com/gitea/gitea-mcp/pkg/to" "gitea.com/gitea/gitea-mcp/pkg/to"
"gitea.com/gitea/gitea-mcp/pkg/tool" "gitea.com/gitea/gitea-mcp/pkg/tool"
@@ -28,7 +27,7 @@ var (
SearchUsersTool = mcp.NewTool( SearchUsersTool = mcp.NewTool(
SearchUsersToolName, SearchUsersToolName,
mcp.WithDescription("search users"), mcp.WithDescription("search users"),
mcp.WithString("keyword", mcp.Required(), mcp.Description("Keyword")), mcp.WithString("keyword", 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)),
) )
@@ -36,8 +35,8 @@ var (
SearOrgTeamsTool = mcp.NewTool( SearOrgTeamsTool = mcp.NewTool(
SearchOrgTeamsToolName, SearchOrgTeamsToolName,
mcp.WithDescription("search organization teams"), mcp.WithDescription("search organization teams"),
mcp.WithString("org", mcp.Required(), mcp.Description("organization name")), mcp.WithString("org", mcp.Description("organization name")),
mcp.WithString("query", mcp.Required(), mcp.Description("search organization teams")), mcp.WithString("query", 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)),
@@ -46,7 +45,7 @@ var (
SearchReposTool = mcp.NewTool( SearchReposTool = mcp.NewTool(
SearchReposToolName, SearchReposToolName,
mcp.WithDescription("search repos"), mcp.WithDescription("search repos"),
mcp.WithString("keyword", mcp.Required(), mcp.Description("Keyword")), mcp.WithString("keyword", 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")),
@@ -62,26 +61,32 @@ var (
func init() { func init() {
Tool.RegisterRead(server.ServerTool{ Tool.RegisterRead(server.ServerTool{
Tool: SearchUsersTool, Tool: SearchUsersTool,
Handler: UsersFn, Handler: SearchUsersFn,
}) })
Tool.RegisterRead(server.ServerTool{ Tool.RegisterRead(server.ServerTool{
Tool: SearOrgTeamsTool, Tool: SearOrgTeamsTool,
Handler: OrgTeamsFn, Handler: SearchOrgTeamsFn,
}) })
Tool.RegisterRead(server.ServerTool{ Tool.RegisterRead(server.ServerTool{
Tool: SearchReposTool, Tool: SearchReposTool,
Handler: ReposFn, Handler: SearchReposFn,
}) })
} }
func UsersFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func SearchUsersFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called UsersFn") log.Debugf("Called SearchUsersFn")
keyword, ok := req.GetArguments()["keyword"].(string) keyword, ok := req.GetArguments()["keyword"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("keyword is required")) return to.ErrorResult(fmt.Errorf("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{
@@ -100,19 +105,25 @@ func UsersFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult,
return to.TextResult(users) return to.TextResult(users)
} }
func OrgTeamsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func SearchOrgTeamsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called OrgTeamsFn") log.Debugf("Called SearchOrgTeamsFn")
org, ok := req.GetArguments()["org"].(string) org, ok := req.GetArguments()["org"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("organization is required")) return to.ErrorResult(fmt.Errorf("organization is required"))
} }
query, ok := req.GetArguments()["query"].(string) query, ok := req.GetArguments()["query"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("query is required")) return to.ErrorResult(fmt.Errorf("query is required"))
} }
includeDescription, _ := req.GetArguments()["includeDescription"].(bool) includeDescription, _ := req.GetArguments()["includeDescription"].(bool)
page := params.GetOptionalInt(req.GetArguments(), "page", 1) page, ok := req.GetArguments()["page"].(float64)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) if !ok {
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,
@@ -132,34 +143,40 @@ func OrgTeamsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResu
return to.TextResult(teams) return to.TextResult(teams)
} }
func ReposFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func SearchReposFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ReposFn") log.Debugf("Called SearchReposFn")
keyword, ok := req.GetArguments()["keyword"].(string) keyword, ok := req.GetArguments()["keyword"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("keyword is required")) return to.ErrorResult(fmt.Errorf("keyword is required"))
} }
keywordIsTopic, _ := req.GetArguments()["keywordIsTopic"].(bool) keywordIsTopic, _ := req.GetArguments()["keywordIsTopic"].(bool)
keywordInDescription, _ := req.GetArguments()["keywordInDescription"].(bool) keywordInDescription, _ := req.GetArguments()["keywordInDescription"].(bool)
ownerID := params.GetOptionalInt(req.GetArguments(), "ownerID", 0) ownerID, _ := req.GetArguments()["ownerID"].(float64)
var pIsPrivate *bool var pIsPrivate *bool
isPrivate, ok := req.GetArguments()["isPrivate"].(bool) isPrivate, ok := req.GetArguments()["isPrivate"].(bool)
if ok { if ok {
pIsPrivate = new(isPrivate) pIsPrivate = ptr.To(isPrivate)
} }
var pIsArchived *bool var pIsArchived *bool
isArchived, ok := req.GetArguments()["isArchived"].(bool) isArchived, ok := req.GetArguments()["isArchived"].(bool)
if ok { if ok {
pIsArchived = new(isArchived) pIsArchived = ptr.To(isArchived)
} }
sort, _ := req.GetArguments()["sort"].(string) sort, _ := req.GetArguments()["sort"].(string)
order, _ := req.GetArguments()["order"].(string) order, _ := req.GetArguments()["order"].(string)
page := params.GetOptionalInt(req.GetArguments(), "page", 1) page, ok := req.GetArguments()["page"].(float64)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) if !ok {
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: ownerID, OwnerID: int64(ownerID),
IsPrivate: pIsPrivate, IsPrivate: pIsPrivate,
IsArchived: pIsArchived, IsArchived: pIsArchived,
Sort: sort, Sort: sort,

View File

@@ -1,42 +0,0 @@
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

@@ -3,7 +3,6 @@ package timetracking
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
gitea_sdk "code.gitea.io/sdk/gitea" gitea_sdk "code.gitea.io/sdk/gitea"
@@ -130,11 +129,11 @@ 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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
index, err := params.GetIndex(req.GetArguments(), "index") index, err := params.GetIndex(req.GetArguments(), "index")
if err != nil { if err != nil {
@@ -155,11 +154,11 @@ func StopStopwatchFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToo
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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
index, err := params.GetIndex(req.GetArguments(), "index") index, err := params.GetIndex(req.GetArguments(), "index")
if err != nil { if err != nil {
@@ -180,11 +179,11 @@ func DeleteStopwatchFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallT
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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
index, err := params.GetIndex(req.GetArguments(), "index") index, err := params.GetIndex(req.GetArguments(), "index")
if err != nil { if err != nil {
@@ -207,7 +206,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.ListMyStopwatches(gitea_sdk.ListStopwatchesOptions{}) stopwatches, _, err := client.GetMyStopwatches()
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))
} }
@@ -223,18 +222,24 @@ 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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
index, err := params.GetIndex(req.GetArguments(), "index") index, err := params.GetIndex(req.GetArguments(), "index")
if err != nil { if err != nil {
return to.ErrorResult(err) return to.ErrorResult(err)
} }
page := params.GetOptionalInt(req.GetArguments(), "page", 1) page, ok := req.GetArguments()["page"].(float64)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) if !ok {
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))
@@ -259,27 +264,27 @@ 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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
index, err := params.GetIndex(req.GetArguments(), "index") index, err := params.GetIndex(req.GetArguments(), "index")
if err != nil { if err != nil {
return to.ErrorResult(err) return to.ErrorResult(err)
} }
timeSeconds, err := params.GetIndex(req.GetArguments(), "time") timeSeconds, ok := req.GetArguments()["time"].(float64)
if err != nil { if !ok {
return to.ErrorResult(err) return to.ErrorResult(fmt.Errorf("time is required"))
} }
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, index, gitea_sdk.AddTimeOption{ trackedTime, _, err := client.AddTime(owner, repo, index, gitea_sdk.AddTimeOption{
Time: timeSeconds, Time: int64(timeSeconds),
}) })
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("add tracked time to %s/%s#%d err: %v", owner, repo, index, err)) return to.ErrorResult(fmt.Errorf("add tracked time to %s/%s#%d err: %v", owner, repo, index, err))
@@ -291,45 +296,51 @@ 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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
index, err := params.GetIndex(req.GetArguments(), "index") index, err := params.GetIndex(req.GetArguments(), "index")
if err != nil { if err != nil {
return to.ErrorResult(err) return to.ErrorResult(err)
} }
id, err := params.GetIndex(req.GetArguments(), "id") id, ok := req.GetArguments()["id"].(float64)
if err != nil { if !ok {
return to.ErrorResult(err) return to.ErrorResult(fmt.Errorf("id is required"))
} }
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, index, id) _, err = client.DeleteTime(owner, repo, index, int64(id))
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("delete tracked time %d from %s/%s#%d err: %v", id, owner, repo, index, err)) return to.ErrorResult(fmt.Errorf("delete tracked time %d from %s/%s#%d err: %v", int64(id), owner, repo, index, err))
} }
return to.TextResult(fmt.Sprintf("Tracked time entry %d deleted from issue %s/%s#%d", id, owner, repo, index)) return to.TextResult(fmt.Sprintf("Tracked time entry %d deleted from issue %s/%s#%d", int64(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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
page := params.GetOptionalInt(req.GetArguments(), "page", 1) page, ok := req.GetArguments()["page"].(float64)
pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) if !ok {
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))
@@ -355,7 +366,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.ListMyTrackedTimes(gitea_sdk.ListTrackedTimesOptions{}) times, _, err := client.GetMyTrackedTimes()
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))
} }

View File

@@ -6,7 +6,6 @@ import (
"gitea.com/gitea/gitea-mcp/pkg/gitea" "gitea.com/gitea/gitea-mcp/pkg/gitea"
"gitea.com/gitea/gitea-mcp/pkg/log" "gitea.com/gitea/gitea-mcp/pkg/log"
"gitea.com/gitea/gitea-mcp/pkg/params"
"gitea.com/gitea/gitea-mcp/pkg/to" "gitea.com/gitea/gitea-mcp/pkg/to"
"gitea.com/gitea/gitea-mcp/pkg/tool" "gitea.com/gitea/gitea-mcp/pkg/tool"
@@ -69,11 +68,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 {
v := params.GetOptionalInt(req.GetArguments(), name, int64(def)) val, ok := req.GetArguments()[name].(float64)
if v < 1 { if !ok || val < 1 {
return def return def
} }
return int(v) return int(val)
} }
// 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

@@ -2,7 +2,6 @@ package wiki
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"net/url" "net/url"
@@ -111,11 +110,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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("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
@@ -132,15 +131,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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
pageName, ok := req.GetArguments()["pageName"].(string) pageName, ok := req.GetArguments()["pageName"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("pageName is required")) return to.ErrorResult(fmt.Errorf("pageName is required"))
} }
var result any var result any
@@ -156,15 +155,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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
pageName, ok := req.GetArguments()["pageName"].(string) pageName, ok := req.GetArguments()["pageName"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("pageName is required")) return to.ErrorResult(fmt.Errorf("pageName is required"))
} }
var result any var result any
@@ -180,19 +179,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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
title, ok := req.GetArguments()["title"].(string) title, ok := req.GetArguments()["title"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("title is required")) return to.ErrorResult(fmt.Errorf("title is required"))
} }
contentBase64, ok := req.GetArguments()["content_base64"].(string) contentBase64, ok := req.GetArguments()["content_base64"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("content_base64 is required")) return to.ErrorResult(fmt.Errorf("content_base64 is required"))
} }
message, _ := req.GetArguments()["message"].(string) message, _ := req.GetArguments()["message"].(string)
@@ -219,19 +218,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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
pageName, ok := req.GetArguments()["pageName"].(string) pageName, ok := req.GetArguments()["pageName"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("pageName is required")) return to.ErrorResult(fmt.Errorf("pageName is required"))
} }
contentBase64, ok := req.GetArguments()["content_base64"].(string) contentBase64, ok := req.GetArguments()["content_base64"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("content_base64 is required")) return to.ErrorResult(fmt.Errorf("content_base64 is required"))
} }
requestBody := map[string]string{ requestBody := map[string]string{
@@ -265,15 +264,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(errors.New("owner is required")) return to.ErrorResult(fmt.Errorf("owner is required"))
} }
repo, ok := req.GetArguments()["repo"].(string) repo, ok := req.GetArguments()["repo"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("repo is required")) return to.ErrorResult(fmt.Errorf("repo is required"))
} }
pageName, ok := req.GetArguments()["pageName"].(string) pageName, ok := req.GetArguments()["pageName"].(string)
if !ok { if !ok {
return to.ErrorResult(errors.New("pageName is required")) return to.ErrorResult(fmt.Errorf("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

@@ -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("gitea-mcp-server/" + flag.Version) client.SetUserAgent(fmt.Sprintf("gitea-mcp-server/%s", flag.Version))
return client, nil return client, nil
} }

View File

@@ -5,7 +5,6 @@ import (
"context" "context"
"crypto/tls" "crypto/tls"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
@@ -41,7 +40,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 // user-requested insecure mode transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} //nolint:gosec
} }
return &http.Client{ return &http.Client{
Transport: transport, Transport: transport,
@@ -52,7 +51,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 "", errors.New("gitea host is empty") return "", fmt.Errorf("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))
@@ -67,7 +66,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, respOut any) (int, error) { func DoJSON(ctx context.Context, method, path string, query url.Values, body any, 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)
@@ -88,7 +87,7 @@ func DoJSON(ctx context.Context, method, path string, query url.Values, body, re
token := tokenFromContext(ctx) token := tokenFromContext(ctx)
if token != "" { if token != "" {
req.Header.Set("Authorization", "token "+token) req.Header.Set("Authorization", fmt.Sprintf("token %s", token))
} }
req.Header.Set("Accept", "application/json") req.Header.Set("Accept", "application/json")
if body != nil { if body != nil {
@@ -108,7 +107,7 @@ func DoJSON(ctx context.Context, method, path string, query url.Values, body, re
} }
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
} }
@@ -141,7 +140,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", "token "+token) req.Header.Set("Authorization", fmt.Sprintf("token %s", token))
} }
if accept != "" { if accept != "" {
req.Header.Set("Accept", accept) req.Header.Set("Accept", accept)
@@ -172,3 +171,5 @@ 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

@@ -28,3 +28,5 @@ func TestTokenFromContext(t *testing.T) {
} }
}) })
} }

View File

@@ -1,6 +1,7 @@
package log package log
import ( import (
"fmt"
"os" "os"
"sync" "sync"
"time" "time"
@@ -34,14 +35,14 @@ func Default() *zap.Logger {
home = os.TempDir() home = os.TempDir()
} }
logDir := home + "/.gitea-mcp" logDir := fmt.Sprintf("%s/.gitea-mcp", home)
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: logDir + "/gitea-mcp.log", Filename: fmt.Sprintf("%s/gitea-mcp.log", logDir),
MaxSize: 100, MaxSize: 100,
MaxBackups: 10, MaxBackups: 10,
MaxAge: 30, MaxAge: 30,

View File

@@ -5,54 +5,29 @@ import (
"strconv" "strconv"
) )
// ToInt64 converts a value to int64, accepting both float64 (JSON number) and // GetIndex extracts an index parameter from MCP tool arguments.
// 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. // It accepts both numeric (float64 from JSON) and string representations.
// This provides better UX for LLM callers that may naturally use strings // This provides better UX for LLM callers that may naturally use strings
// for identifiers like issue/PR numbers. // for identifiers like issue/PR numbers.
func GetIndex(args map[string]any, key string) (int64, error) { func GetIndex(args map[string]interface{}, key string) (int64, error) {
val, exists := args[key] val, exists := args[key]
if !exists { if !exists {
return 0, fmt.Errorf("%s is required", key) return 0, fmt.Errorf("%s is required", key)
} }
if i, ok := ToInt64(val); ok { // Try float64 (JSON number type)
return i, nil if f, ok := val.(float64); ok {
return int64(f), nil
} }
// Try string and parse to integer
if s, ok := val.(string); ok { if s, ok := val.(string); ok {
return 0, fmt.Errorf("%s must be a valid integer (got %q)", key, s) i, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return 0, fmt.Errorf("%s must be a valid integer (got %q)", key, s)
}
return i, nil
} }
return 0, fmt.Errorf("%s must be a number or numeric string", key) 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
}

View File

@@ -5,67 +5,10 @@ import (
"testing" "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) { func TestGetIndex(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
args map[string]any args map[string]interface{}
key string key string
wantIndex int64 wantIndex int64
wantErr bool wantErr bool
@@ -73,63 +16,63 @@ func TestGetIndex(t *testing.T) {
}{ }{
{ {
name: "valid float64", name: "valid float64",
args: map[string]any{"index": float64(123)}, args: map[string]interface{}{"index": float64(123)},
key: "index", key: "index",
wantIndex: 123, wantIndex: 123,
wantErr: false, wantErr: false,
}, },
{ {
name: "valid string", name: "valid string",
args: map[string]any{"index": "456"}, args: map[string]interface{}{"index": "456"},
key: "index", key: "index",
wantIndex: 456, wantIndex: 456,
wantErr: false, wantErr: false,
}, },
{ {
name: "valid string with large number", name: "valid string with large number",
args: map[string]any{"index": "999999"}, args: map[string]interface{}{"index": "999999"},
key: "index", key: "index",
wantIndex: 999999, wantIndex: 999999,
wantErr: false, wantErr: false,
}, },
{ {
name: "missing parameter", name: "missing parameter",
args: map[string]any{}, args: map[string]interface{}{},
key: "index", key: "index",
wantErr: true, wantErr: true,
errMsg: "index is required", errMsg: "index is required",
}, },
{ {
name: "invalid string (not a number)", name: "invalid string (not a number)",
args: map[string]any{"index": "abc"}, args: map[string]interface{}{"index": "abc"},
key: "index", key: "index",
wantErr: true, wantErr: true,
errMsg: "must be a valid integer", errMsg: "must be a valid integer",
}, },
{ {
name: "invalid string (decimal)", name: "invalid string (decimal)",
args: map[string]any{"index": "12.34"}, args: map[string]interface{}{"index": "12.34"},
key: "index", key: "index",
wantErr: true, wantErr: true,
errMsg: "must be a valid integer", errMsg: "must be a valid integer",
}, },
{ {
name: "invalid type (bool)", name: "invalid type (bool)",
args: map[string]any{"index": true}, args: map[string]interface{}{"index": true},
key: "index", key: "index",
wantErr: true, wantErr: true,
errMsg: "must be a number or numeric string", errMsg: "must be a number or numeric string",
}, },
{ {
name: "invalid type (map)", name: "invalid type (map)",
args: map[string]any{"index": map[string]string{"foo": "bar"}}, args: map[string]interface{}{"index": map[string]string{"foo": "bar"}},
key: "index", key: "index",
wantErr: true, wantErr: true,
errMsg: "must be a number or numeric string", errMsg: "must be a number or numeric string",
}, },
{ {
name: "custom key name", name: "custom key name",
args: map[string]any{"pr_index": "789"}, args: map[string]interface{}{"pr_index": "789"},
key: "pr_index", key: "pr_index",
wantIndex: 789, wantIndex: 789,
wantErr: false, wantErr: false,

73
pkg/ptr/ptr.go Normal file
View File

@@ -0,0 +1,73 @@
/*
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
}