10 Commits
sec ... main

Author SHA1 Message Date
Darren Hoo
d7addd56c4 feat: read token from header in http/sse mode (#89)
this PR introduces support for per-request authentication tokens in HTTP and SSE modes. The server now inspects incoming requests for an `Authorization: Bearer <token>` header.

Previously, the server operated with a single, globally configured Gitea token. This change allows different clients to use their own tokens when communicating with the MCP server, enhancing security and flexibility.

To support this, the Gitea API client initialization has been refactored:
- The global singleton Gitea client has been removed.
-  A new `ClientFromContext` function creates a Gitea client on-demand, using a token from the request context if available, and falling back to the globally configured token otherwise.
- All tool functions now retrieve the client from the context for each call.

The README has also been updated to reflect the new configuration option.

Update: #59
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/gitea-mcp/pulls/89
Reviewed-by: hiifong <i@hiif.ong>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Darren Hoo <darren.hoo@gmail.com>
Co-committed-by: Darren Hoo <darren.hoo@gmail.com>
2025-09-12 03:57:57 +00:00
hiifong
dc3e120e97 Update operation/repo/file.go 2025-08-29 05:57:44 +00:00
marcluer
f33b04a3df feat: added parameter 'organization' to tool 'create_repo' (#88)
Using the Gitea-mcp server I was missing the ability to create repositories in other organizations. e.g.:
* I was only able to create `https://gitea.domain.com/myuser/repo` 
* I was not able to create `https://gitea.domain.com/organization/repo` 

This feature was planned, implemented and compiled by Claude Code. I have no clue about Golang.

I then took the resulting `gitea-mcp` file and sucessfully tested it on my self-hosted gitea instance:
* Creating `https://gitea.domain.com/myuser/repo` 
* Creating `https://gitea.domain.com/organization/repo` 

Reviewed-on: https://gitea.com/gitea/gitea-mcp/pulls/88
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: marcluer <gitea@marcluerssen.de>
Co-committed-by: marcluer <gitea@marcluerssen.de>
2025-08-29 05:37:19 +00:00
appleboy
ba07925969 refactor: refactor MCP tool registration and pagination handling (#86)
- Add documentation for MCP tool constants and tool registration
- Use configurable default values for pagination arguments in user organization queries
- Introduce registerTools helper to streamline MCP tool registration
- Refactor pagination argument parsing into a reusable getIntArg function
- Add descriptive logging for tool handler execution
- Improve code organization for defining and registering MCP tools

Signed-off-by: appleboy <appleboy.tw@gmail.com>

Reviewed-on: https://gitea.com/gitea/gitea-mcp/pulls/86
Co-authored-by: appleboy <appleboy.tw@gmail.com>
Co-committed-by: appleboy <appleboy.tw@gmail.com>
2025-08-23 04:31:57 +00:00
meestark
5c2ff6dcb2 feat: Add support for managing repository and issue labels (#83)
## **What:**
Adds full label management capabilities to the Gitea CLI for both repositories and issues. Users can now create, edit, delete, list, and assign labels without leaving the terminal.

## **Why:**
Labels are a core part of keeping repositories and issues organized. Previously, `gitea-mcp` lacked CLI support for label management, forcing users to rely on the web UI or custom scripts. This update closes that gap, enabling smoother automation and more efficient workflows.

## **How:**
Implemented new `label` subcommands:

* **Repository Labels:**
  * `list_repo_labels` — Lists all labels for a repository.
  * `get_repo_label` — Retrieves a label by ID.
  * `create_repo_label` — Creates a new label.
  * `edit_repo_label` — Updates an existing label.
  * `delete_repo_label` — Removes a label.

* **Issue Labels:**
  * `add_issue_labels` — Adds one or more labels to an issue.
  * `replace_issue_labels` — Replaces all labels on an issue.
  * `clear_issue_labels` — Removes all labels from an issue.
  * `remove_issue_label` — Removes a single label from an issue.

## **Testing:**
User acceptance testing was performed across all new commands, confirming correct behavior for creating, editing, deleting, listing, and applying labels.  Also looped through 20 issues in roo Orchestrator mode and assigned different labels to each without issue.

Reviewed-on: https://gitea.com/gitea/gitea-mcp/pulls/83
Reviewed-by: hiifong <i@hiif.ong>
Co-authored-by: meestark <meestark@meestark.net>
Co-committed-by: meestark <meestark@meestark.net>
2025-08-11 07:33:07 +00:00
meestark
feaedaf604 fix: pass body through in create_release (#82)
### What
Ensure `create_release` accepts and forwards a `body` so release notes are created as provided.

### Why
Previously, the `body` parameter wasn’t threaded through, resulting in empty release notes even when a body was supplied.

### How
- Add `body` parameter to the function signature
- Thread `body` through handler/service to the API call
- Light refactor for clarity; no breaking changes

### Testing
- Manual: created a release with a non-empty body and confirmed it appears in the UI and in the releases API response

### Links
Fixes gitea/gitea-mcp#81

Reviewed-on: https://gitea.com/gitea/gitea-mcp/pulls/82
Reviewed-by: hiifong <i@hiif.ong>
Co-authored-by: meestark <meestark@meestark.net>
Co-committed-by: meestark <meestark@meestark.net>
2025-08-11 01:07:52 +00:00
yp05327
a601d6b698 Remove last empty line in GetFileContentFn (#80)
Normally, each file should be end with a blank line, but git does not consider it as a new line, so we should not return it to llm, or it may generate wrong information when editing the existing file.

Reviewed-on: https://gitea.com/gitea/gitea-mcp/pulls/80
Reviewed-by: hiifong <i@hiif.ong>
Co-authored-by: yp05327 <576951401@qq.com>
Co-committed-by: yp05327 <576951401@qq.com>
2025-08-07 02:18:01 +00:00
Lunny Xiao
62cb6e7830 Use no session id (#75)
Reviewed-on: https://gitea.com/gitea/gitea-mcp/pulls/75
Reviewed-by: hiifong <i@hiif.ong>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-committed-by: Lunny Xiao <xiaolunwen@gmail.com>
2025-07-30 01:01:55 +00:00
yp05327
9fff996294 Add withLines option to get_file_content (#76)
Reviewed-on: https://gitea.com/gitea/gitea-mcp/pulls/76
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: yp05327 <576951401@qq.com>
Co-committed-by: yp05327 <576951401@qq.com>
2025-07-29 18:29:29 +00:00
appleboy
4c3f5149d8 feat: set custom user agent for Gitea client using server version (#74)
- Import the fmt package to enable string formatting
- Set a custom user agent for the Gitea client using the current server version

Signed-off-by: appleboy <appleboy.tw@gmail.com>

Reviewed-on: https://gitea.com/gitea/gitea-mcp/pulls/74
Co-authored-by: appleboy <appleboy.tw@gmail.com>
Co-committed-by: appleboy <appleboy.tw@gmail.com>
2025-07-27 10:31:48 +00:00
20 changed files with 862 additions and 125 deletions

View File

@@ -24,18 +24,3 @@ jobs:
with: with:
go-version-file: 'go.mod' go-version-file: 'go.mod'
go-package: ./... go-package: ./...
code-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Trivy vulnerability scanner in repo mode
uses: aquasecurity/trivy-action@0.28.0
with:
scan-type: 'fs'
ignore-unfixed: true
format: 'sarif'
output: 'trivy-results.sarif'
exit-code: '1'
severity: 'CRITICAL,HIGH'

View File

@@ -139,7 +139,10 @@ To configure the MCP server for Gitea, add the following to your MCP configurati
{ {
"mcpServers": { "mcpServers": {
"gitea": { "gitea": {
"url": "http://localhost:8080/sse" "url": "http://localhost:8080/sse",
"headers": {
"Authorization": "Bearer <your personal access token>"
}
} }
} }
} }
@@ -151,7 +154,10 @@ To configure the MCP server for Gitea, add the following to your MCP configurati
{ {
"mcpServers": { "mcpServers": {
"gitea": { "gitea": {
"url": "http://localhost:8080/mcp" "url": "http://localhost:8080/mcp",
"headers": {
"Authorization": "Bearer <your personal access token>"
}
} }
} }
} }

View File

@@ -139,7 +139,10 @@ cp gitea-mcp /usr/local/bin/
{ {
"mcpServers": { "mcpServers": {
"gitea": { "gitea": {
"url": "http://localhost:8080/sse" "url": "http://localhost:8080/sse",
"headers": {
"Authorization": "Bearer <your personal access token>"
}
} }
} }
} }
@@ -151,7 +154,10 @@ cp gitea-mcp /usr/local/bin/
{ {
"mcpServers": { "mcpServers": {
"gitea": { "gitea": {
"url": "http://localhost:8080/mcp" "url": "http://localhost:8080/mcp",
"headers": {
"Authorization": "Bearer <your personal access token>"
}
} }
} }
} }

View File

@@ -139,7 +139,10 @@ cp gitea-mcp /usr/local/bin/
{ {
"mcpServers": { "mcpServers": {
"gitea": { "gitea": {
"url": "http://localhost:8080/sse" "url": "http://localhost:8080/sse",
"headers": {
"Authorization": "Bearer <your personal access token>"
}
} }
} }
} }
@@ -151,7 +154,10 @@ cp gitea-mcp /usr/local/bin/
{ {
"mcpServers": { "mcpServers": {
"gitea": { "gitea": {
"url": "http://localhost:8080/mcp" "url": "http://localhost:8080/mcp",
"headers": {
"Authorization": "Bearer <your personal access token>"
}
} }
} }
} }

8
go.mod
View File

@@ -4,20 +4,26 @@ go 1.24.0
require ( require (
code.gitea.io/sdk/gitea v0.21.0 code.gitea.io/sdk/gitea v0.21.0
github.com/mark3labs/mcp-go v0.35.0 github.com/mark3labs/mcp-go v0.36.0
go.uber.org/zap v1.27.0 go.uber.org/zap v1.27.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/natefinch/lumberjack.v2 v2.2.1
) )
require ( require (
github.com/42wim/httpsig v1.2.3 // indirect github.com/42wim/httpsig v1.2.3 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/davidmz/go-pageant v1.0.2 // indirect github.com/davidmz/go-pageant v1.0.2 // indirect
github.com/go-fed/httpsig v1.1.0 // indirect github.com/go-fed/httpsig v1.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/go-version v1.7.0 // indirect github.com/hashicorp/go-version v1.7.0 // indirect
github.com/invopop/jsonschema v0.13.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/spf13/cast v1.9.2 // indirect github.com/spf13/cast v1.9.2 // 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.40.0 // indirect golang.org/x/crypto v0.40.0 // indirect
golang.org/x/sys v0.34.0 // indirect golang.org/x/sys v0.34.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
) )

17
go.sum
View File

@@ -2,6 +2,10 @@ code.gitea.io/sdk/gitea v0.21.0 h1:69n6oz6kEVHRo1+APQQyizkhrZrLsTLXey9142pfkD4=
code.gitea.io/sdk/gitea v0.21.0/go.mod h1:tnBjVhuKJCn8ibdyyhvUyxrR1Ca2KHEoTWoukNhXQPA= code.gitea.io/sdk/gitea v0.21.0/go.mod h1:tnBjVhuKJCn8ibdyyhvUyxrR1Ca2KHEoTWoukNhXQPA=
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/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0= github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0=
@@ -16,12 +20,17 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
github.com/hashicorp/go-version v1.7.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/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 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/mark3labs/mcp-go v0.35.0 h1:eh5bJGGVkNEaehCbPmAFqFgk/SB18YvxmsR2rnPm8BQ= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mark3labs/mcp-go v0.35.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mark3labs/mcp-go v0.36.0 h1:rIZaijrRYPeSbJG8/qNDe0hWlGrCJ7FWHNMz2SQpTis=
github.com/mark3labs/mcp-go v0.36.0/go.mod h1:T7tUa2jO6MavG+3P25Oy/jR7iCeJPHImCZHRymCn39g=
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=
@@ -30,6 +39,8 @@ github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE=
github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
@@ -56,6 +67,8 @@ golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
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=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@@ -140,7 +140,11 @@ func GetIssueByIndexFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallT
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("index is required")) return to.ErrorResult(fmt.Errorf("index is required"))
} }
issue, _, err := gitea.Client().GetIssue(owner, repo, int64(index)) client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
issue, _, err := client.GetIssue(owner, repo, int64(index))
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("get %v/%v/issue/%v err: %v", owner, repo, int64(index), err)) return to.ErrorResult(fmt.Errorf("get %v/%v/issue/%v err: %v", owner, repo, int64(index), err))
} }
@@ -177,7 +181,11 @@ func ListRepoIssuesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo
PageSize: int(pageSize), PageSize: int(pageSize),
}, },
} }
issues, _, err := gitea.Client().ListRepoIssues(owner, repo, opt) client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
issues, _, err := client.ListRepoIssues(owner, repo, opt)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("get %v/%v/issues err: %v", owner, repo, err)) return to.ErrorResult(fmt.Errorf("get %v/%v/issues err: %v", owner, repo, err))
} }
@@ -202,7 +210,11 @@ func CreateIssueFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolR
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("body is required")) return to.ErrorResult(fmt.Errorf("body is required"))
} }
issue, _, err := gitea.Client().CreateIssue(owner, repo, gitea_sdk.CreateIssueOption{ client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
issue, _, err := client.CreateIssue(owner, repo, gitea_sdk.CreateIssueOption{
Title: title, Title: title,
Body: body, Body: body,
}) })
@@ -234,7 +246,11 @@ func CreateIssueCommentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Ca
opt := gitea_sdk.CreateIssueCommentOption{ opt := gitea_sdk.CreateIssueCommentOption{
Body: body, Body: body,
} }
issueComment, _, err := gitea.Client().CreateIssueComment(owner, repo, int64(index), opt) client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
issueComment, _, err := client.CreateIssueComment(owner, repo, int64(index), opt)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("create %v/%v/issue/%v/comment err: %v", owner, repo, int64(index), err)) return to.ErrorResult(fmt.Errorf("create %v/%v/issue/%v/comment err: %v", owner, repo, int64(index), err))
} }
@@ -280,7 +296,11 @@ func EditIssueFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRes
opt.State = ptr.To(gitea_sdk.StateType(state)) opt.State = ptr.To(gitea_sdk.StateType(state))
} }
issue, _, err := gitea.Client().EditIssue(owner, repo, int64(index), opt) client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
issue, _, err := client.EditIssue(owner, repo, int64(index), opt)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("edit %v/%v/issue/%v err: %v", owner, repo, int64(index), err)) return to.ErrorResult(fmt.Errorf("edit %v/%v/issue/%v err: %v", owner, repo, int64(index), err))
} }
@@ -309,7 +329,11 @@ func EditIssueCommentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call
opt := gitea_sdk.EditIssueCommentOption{ opt := gitea_sdk.EditIssueCommentOption{
Body: body, Body: body,
} }
issueComment, _, err := gitea.Client().EditIssueComment(owner, repo, int64(commentID), opt) client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
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, int64(commentID), err)) return to.ErrorResult(fmt.Errorf("edit %v/%v/issues/comments/%v err: %v", owner, repo, int64(commentID), err))
} }
@@ -332,7 +356,11 @@ func GetIssueCommentsByIndexFn(ctx context.Context, req mcp.CallToolRequest) (*m
return to.ErrorResult(fmt.Errorf("index is required")) return to.ErrorResult(fmt.Errorf("index is required"))
} }
opt := gitea_sdk.ListIssueCommentOptions{} opt := gitea_sdk.ListIssueCommentOptions{}
issue, _, err := gitea.Client().ListIssueComments(owner, repo, int64(index), opt) client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
issue, _, err := client.ListIssueComments(owner, repo, int64(index), opt)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("get %v/%v/issues/%v/comments err: %v", owner, repo, int64(index), err)) return to.ErrorResult(fmt.Errorf("get %v/%v/issues/%v/comments err: %v", owner, repo, int64(index), err))
} }

454
operation/label/label.go Normal file
View File

@@ -0,0 +1,454 @@
package label
import (
"context"
"fmt"
"gitea.com/gitea/gitea-mcp/pkg/gitea"
"gitea.com/gitea/gitea-mcp/pkg/log"
"gitea.com/gitea/gitea-mcp/pkg/ptr"
"gitea.com/gitea/gitea-mcp/pkg/to"
"gitea.com/gitea/gitea-mcp/pkg/tool"
gitea_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
var Tool = tool.New()
const (
ListRepoLabelsToolName = "list_repo_labels"
GetRepoLabelToolName = "get_repo_label"
CreateRepoLabelToolName = "create_repo_label"
EditRepoLabelToolName = "edit_repo_label"
DeleteRepoLabelToolName = "delete_repo_label"
AddIssueLabelsToolName = "add_issue_labels"
ReplaceIssueLabelsToolName = "replace_issue_labels"
ClearIssueLabelsToolName = "clear_issue_labels"
RemoveIssueLabelToolName = "remove_issue_label"
)
var (
ListRepoLabelsTool = mcp.NewTool(
ListRepoLabelsToolName,
mcp.WithDescription("Lists all labels for a given repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)),
)
GetRepoLabelTool = mcp.NewTool(
GetRepoLabelToolName,
mcp.WithDescription("Gets a single label by its ID for a repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("id", mcp.Required(), mcp.Description("label ID")),
)
CreateRepoLabelTool = mcp.NewTool(
CreateRepoLabelToolName,
mcp.WithDescription("Creates a new label for a repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("name", mcp.Required(), mcp.Description("label name")),
mcp.WithString("color", mcp.Required(), mcp.Description("label color (hex code, e.g., #RRGGBB)")),
mcp.WithString("description", mcp.Description("label description")),
)
EditRepoLabelTool = mcp.NewTool(
EditRepoLabelToolName,
mcp.WithDescription("Edits an existing label in a repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("id", mcp.Required(), mcp.Description("label ID")),
mcp.WithString("name", mcp.Description("new label name")),
mcp.WithString("color", mcp.Description("new label color (hex code, e.g., #RRGGBB)")),
mcp.WithString("description", mcp.Description("new label description")),
)
DeleteRepoLabelTool = mcp.NewTool(
DeleteRepoLabelToolName,
mcp.WithDescription("Deletes a label from a repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("id", mcp.Required(), mcp.Description("label ID")),
)
AddIssueLabelsTool = mcp.NewTool(
AddIssueLabelsToolName,
mcp.WithDescription("Adds one or more labels to an issue"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("index", mcp.Required(), mcp.Description("issue index")),
mcp.WithArray("labels", mcp.Required(), mcp.Description("array of label IDs to add"), mcp.Items(map[string]interface{}{"type": "number"})),
)
ReplaceIssueLabelsTool = mcp.NewTool(
ReplaceIssueLabelsToolName,
mcp.WithDescription("Replaces all labels on an issue"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("index", mcp.Required(), mcp.Description("issue index")),
mcp.WithArray("labels", mcp.Required(), mcp.Description("array of label IDs to replace with"), mcp.Items(map[string]interface{}{"type": "number"})),
)
ClearIssueLabelsTool = mcp.NewTool(
ClearIssueLabelsToolName,
mcp.WithDescription("Removes all labels from an issue"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("index", mcp.Required(), mcp.Description("issue index")),
)
RemoveIssueLabelTool = mcp.NewTool(
RemoveIssueLabelToolName,
mcp.WithDescription("Removes a single label from an issue"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("index", mcp.Required(), mcp.Description("issue index")),
mcp.WithNumber("label_id", mcp.Required(), mcp.Description("label ID to remove")),
)
)
func init() {
Tool.RegisterRead(server.ServerTool{
Tool: ListRepoLabelsTool,
Handler: ListRepoLabelsFn,
})
Tool.RegisterRead(server.ServerTool{
Tool: GetRepoLabelTool,
Handler: GetRepoLabelFn,
})
Tool.RegisterWrite(server.ServerTool{
Tool: CreateRepoLabelTool,
Handler: CreateRepoLabelFn,
})
Tool.RegisterWrite(server.ServerTool{
Tool: EditRepoLabelTool,
Handler: EditRepoLabelFn,
})
Tool.RegisterWrite(server.ServerTool{
Tool: DeleteRepoLabelTool,
Handler: DeleteRepoLabelFn,
})
Tool.RegisterWrite(server.ServerTool{
Tool: AddIssueLabelsTool,
Handler: AddIssueLabelsFn,
})
Tool.RegisterWrite(server.ServerTool{
Tool: ReplaceIssueLabelsTool,
Handler: ReplaceIssueLabelsFn,
})
Tool.RegisterWrite(server.ServerTool{
Tool: ClearIssueLabelsTool,
Handler: ClearIssueLabelsFn,
})
Tool.RegisterWrite(server.ServerTool{
Tool: RemoveIssueLabelTool,
Handler: RemoveIssueLabelFn,
})
}
func ListRepoLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListRepoLabelsFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("owner is required"))
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
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
}
opt := gitea_sdk.ListLabelsOptions{
ListOptions: gitea_sdk.ListOptions{
Page: int(page),
PageSize: int(pageSize),
},
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
labels, _, err := client.ListRepoLabels(owner, repo, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("list %v/%v/labels err: %v", owner, repo, err))
}
return to.TextResult(labels)
}
func GetRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetRepoLabelFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("owner is required"))
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("repo is required"))
}
id, ok := req.GetArguments()["id"].(float64)
if !ok {
return to.ErrorResult(fmt.Errorf("label ID is required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
label, _, err := client.GetRepoLabel(owner, repo, int64(id))
if err != nil {
return to.ErrorResult(fmt.Errorf("get %v/%v/label/%v err: %v", owner, repo, int64(id), err))
}
return to.TextResult(label)
}
func CreateRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CreateRepoLabelFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("owner is required"))
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("repo is required"))
}
name, ok := req.GetArguments()["name"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("name is required"))
}
color, ok := req.GetArguments()["color"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("color is required"))
}
description, _ := req.GetArguments()["description"].(string) // Optional
opt := gitea_sdk.CreateLabelOption{
Name: name,
Color: color,
Description: description,
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
label, _, err := client.CreateLabel(owner, repo, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("create %v/%v/label err: %v", owner, repo, err))
}
return to.TextResult(label)
}
func EditRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called EditRepoLabelFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("owner is required"))
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("repo is required"))
}
id, ok := req.GetArguments()["id"].(float64)
if !ok {
return to.ErrorResult(fmt.Errorf("label ID is required"))
}
opt := gitea_sdk.EditLabelOption{}
if name, ok := req.GetArguments()["name"].(string); ok {
opt.Name = ptr.To(name)
}
if color, ok := req.GetArguments()["color"].(string); ok {
opt.Color = ptr.To(color)
}
if description, ok := req.GetArguments()["description"].(string); ok {
opt.Description = ptr.To(description)
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
label, _, err := client.EditLabel(owner, repo, int64(id), opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("edit %v/%v/label/%v err: %v", owner, repo, int64(id), err))
}
return to.TextResult(label)
}
func DeleteRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called DeleteRepoLabelFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("owner is required"))
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("repo is required"))
}
id, ok := req.GetArguments()["id"].(float64)
if !ok {
return to.ErrorResult(fmt.Errorf("label ID is required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.DeleteLabel(owner, repo, int64(id))
if err != nil {
return to.ErrorResult(fmt.Errorf("delete %v/%v/label/%v err: %v", owner, repo, int64(id), err))
}
return to.TextResult("Label deleted successfully")
}
func AddIssueLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called AddIssueLabelsFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("owner is required"))
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("repo is required"))
}
index, ok := req.GetArguments()["index"].(float64)
if !ok {
return to.ErrorResult(fmt.Errorf("issue index is required"))
}
labelsRaw, ok := req.GetArguments()["labels"].([]interface{})
if !ok {
return to.ErrorResult(fmt.Errorf("labels (array of IDs) is required"))
}
var labels []int64
for _, l := range labelsRaw {
if labelID, ok := l.(float64); ok {
labels = append(labels, int64(labelID))
} else {
return to.ErrorResult(fmt.Errorf("invalid label ID in labels array"))
}
}
opt := gitea_sdk.IssueLabelsOption{
Labels: labels,
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
issueLabels, _, err := client.AddIssueLabels(owner, repo, int64(index), opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("add labels to %v/%v/issue/%v err: %v", owner, repo, int64(index), err))
}
return to.TextResult(issueLabels)
}
func ReplaceIssueLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ReplaceIssueLabelsFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("owner is required"))
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("repo is required"))
}
index, ok := req.GetArguments()["index"].(float64)
if !ok {
return to.ErrorResult(fmt.Errorf("issue index is required"))
}
labelsRaw, ok := req.GetArguments()["labels"].([]interface{})
if !ok {
return to.ErrorResult(fmt.Errorf("labels (array of IDs) is required"))
}
var labels []int64
for _, l := range labelsRaw {
if labelID, ok := l.(float64); ok {
labels = append(labels, int64(labelID))
} else {
return to.ErrorResult(fmt.Errorf("invalid label ID in labels array"))
}
}
opt := gitea_sdk.IssueLabelsOption{
Labels: labels,
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
issueLabels, _, err := client.ReplaceIssueLabels(owner, repo, int64(index), opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("replace labels on %v/%v/issue/%v err: %v", owner, repo, int64(index), err))
}
return to.TextResult(issueLabels)
}
func ClearIssueLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ClearIssueLabelsFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("owner is required"))
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("repo is required"))
}
index, ok := req.GetArguments()["index"].(float64)
if !ok {
return to.ErrorResult(fmt.Errorf("issue index is required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.ClearIssueLabels(owner, repo, int64(index))
if err != nil {
return to.ErrorResult(fmt.Errorf("clear labels on %v/%v/issue/%v err: %v", owner, repo, int64(index), err))
}
return to.TextResult("Labels cleared successfully")
}
func RemoveIssueLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called RemoveIssueLabelFn")
owner, ok := req.GetArguments()["owner"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("owner is required"))
}
repo, ok := req.GetArguments()["repo"].(string)
if !ok {
return to.ErrorResult(fmt.Errorf("repo is required"))
}
index, ok := req.GetArguments()["index"].(float64)
if !ok {
return to.ErrorResult(fmt.Errorf("issue index is required"))
}
labelID, ok := req.GetArguments()["label_id"].(float64)
if !ok {
return to.ErrorResult(fmt.Errorf("label ID is required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.DeleteIssueLabel(owner, repo, int64(index), int64(labelID))
if err != nil {
return to.ErrorResult(fmt.Errorf("remove label %v from %v/%v/issue/%v err: %v", int64(labelID), owner, repo, int64(index), err))
}
return to.TextResult("Label removed successfully")
}

View File

@@ -1,15 +1,20 @@
package operation package operation
import ( import (
"context"
"fmt" "fmt"
"net/http"
"strings"
"time" "time"
"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/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/user" "gitea.com/gitea/gitea-mcp/operation/user"
"gitea.com/gitea/gitea-mcp/operation/version" "gitea.com/gitea/gitea-mcp/operation/version"
mcpContext "gitea.com/gitea/gitea-mcp/pkg/context"
"gitea.com/gitea/gitea-mcp/pkg/flag" "gitea.com/gitea/gitea-mcp/pkg/flag"
"gitea.com/gitea/gitea-mcp/pkg/log" "gitea.com/gitea/gitea-mcp/pkg/log"
@@ -28,6 +33,9 @@ func RegisterTool(s *server.MCPServer) {
// Issue Tool // Issue Tool
s.AddTools(issue.Tool.Tools()...) s.AddTools(issue.Tool.Tools()...)
// Label Tool
s.AddTools(label.Tool.Tools()...)
// Pull Tool // Pull Tool
s.AddTools(pull.Tool.Tools()...) s.AddTools(pull.Tool.Tools()...)
@@ -40,6 +48,20 @@ func RegisterTool(s *server.MCPServer) {
s.DeleteTools("") s.DeleteTools("")
} }
func getContextWithToken(ctx context.Context, r *http.Request) context.Context {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
return ctx
}
parts := strings.Split(authHeader, " ")
if len(parts) != 2 || parts[0] != "Bearer" {
return ctx
}
return context.WithValue(ctx, mcpContext.TokenContextKey, parts[1])
}
func Run() error { func Run() error {
mcpServer = newMCPServer(flag.Version) mcpServer = newMCPServer(flag.Version)
RegisterTool(mcpServer) RegisterTool(mcpServer)
@@ -53,6 +75,7 @@ func Run() error {
case "sse": case "sse":
sseServer := server.NewSSEServer( sseServer := server.NewSSEServer(
mcpServer, mcpServer,
server.WithSSEContextFunc(getContextWithToken),
) )
log.Infof("Gitea MCP SSE server listening on :%d", flag.Port) log.Infof("Gitea MCP SSE server listening on :%d", flag.Port)
if err := sseServer.Start(fmt.Sprintf(":%d", flag.Port)); err != nil { if err := sseServer.Start(fmt.Sprintf(":%d", flag.Port)); err != nil {
@@ -63,6 +86,7 @@ func Run() error {
mcpServer, mcpServer,
server.WithLogger(log.New()), server.WithLogger(log.New()),
server.WithHeartbeatInterval(30*time.Second), server.WithHeartbeatInterval(30*time.Second),
server.WithHTTPContextFunc(getContextWithToken),
) )
log.Infof("Gitea MCP HTTP server listening on :%d", flag.Port) log.Infof("Gitea MCP HTTP server listening on :%d", flag.Port)
if err := httpServer.Start(fmt.Sprintf(":%d", flag.Port)); err != nil { if err := httpServer.Start(fmt.Sprintf(":%d", flag.Port)); err != nil {

View File

@@ -84,7 +84,11 @@ func GetPullRequestByIndexFn(ctx context.Context, req mcp.CallToolRequest) (*mcp
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("index is required")) return to.ErrorResult(fmt.Errorf("index is required"))
} }
pr, _, err := gitea.Client().GetPullRequest(owner, repo, int64(index)) client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
pr, _, err := client.GetPullRequest(owner, repo, int64(index))
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("get %v/%v/pr/%v err: %v", owner, repo, int64(index), err)) return to.ErrorResult(fmt.Errorf("get %v/%v/pr/%v err: %v", owner, repo, int64(index), err))
} }
@@ -125,7 +129,11 @@ func ListRepoPullRequestsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.
PageSize: int(pageSize), PageSize: int(pageSize),
}, },
} }
pullRequests, _, err := gitea.Client().ListRepoPullRequests(owner, repo, opt) client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
pullRequests, _, err := client.ListRepoPullRequests(owner, repo, opt)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("list %v/%v/pull_requests err: %v", owner, repo, err)) return to.ErrorResult(fmt.Errorf("list %v/%v/pull_requests err: %v", owner, repo, err))
} }
@@ -159,7 +167,11 @@ func CreatePullRequestFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Cal
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("base is required")) return to.ErrorResult(fmt.Errorf("base is required"))
} }
pr, _, err := gitea.Client().CreatePullRequest(owner, repo, gitea_sdk.CreatePullRequestOption{ client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
pr, _, err := client.CreatePullRequest(owner, repo, gitea_sdk.CreatePullRequestOption{
Title: title, Title: title,
Body: body, Body: body,
Head: head, Head: head,

View File

@@ -76,7 +76,11 @@ func CreateBranchFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTool
} }
oldBranch, _ := req.GetArguments()["old_branch"].(string) oldBranch, _ := req.GetArguments()["old_branch"].(string)
_, _, err := gitea.Client().CreateBranch(owner, repo, gitea_sdk.CreateBranchOption{ client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, _, err = client.CreateBranch(owner, repo, gitea_sdk.CreateBranchOption{
BranchName: branch, BranchName: branch,
OldBranchName: oldBranch, OldBranchName: oldBranch,
}) })
@@ -101,7 +105,11 @@ func DeleteBranchFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTool
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("branch is required")) return to.ErrorResult(fmt.Errorf("branch is required"))
} }
_, _, err := gitea.Client().DeleteRepoBranch(owner, repo, branch) client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, _, err = client.DeleteRepoBranch(owner, repo, branch)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("delete branch error: %v", err)) return to.ErrorResult(fmt.Errorf("delete branch error: %v", err))
} }
@@ -125,7 +133,11 @@ func ListBranchesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTool
PageSize: 100, PageSize: 100,
}, },
} }
branches, _, err := gitea.Client().ListRepoBranches(owner, repo, opt) client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
branches, _, err := client.ListRepoBranches(owner, repo, opt)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("list branches error: %v", err)) return to.ErrorResult(fmt.Errorf("list branches error: %v", err))
} }

View File

@@ -63,7 +63,11 @@ func ListRepoCommitsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallT
SHA: sha, SHA: sha,
Path: path, Path: path,
} }
commits, _, err := gitea.Client().ListRepoCommits(owner, repo, opt) client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
commits, _, err := client.ListRepoCommits(owner, repo, opt)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("list repo commits err: %v", err)) return to.ErrorResult(fmt.Errorf("list repo commits err: %v", err))
} }

View File

@@ -1,8 +1,11 @@
package repo package repo
import ( import (
"bufio"
"bytes"
"context" "context"
"encoding/base64" "encoding/base64"
"encoding/json"
"fmt" "fmt"
"gitea.com/gitea/gitea-mcp/pkg/gitea" "gitea.com/gitea/gitea-mcp/pkg/gitea"
@@ -30,6 +33,7 @@ var (
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("ref", mcp.Required(), mcp.Description("ref can be branch/tag/commit")), mcp.WithString("ref", mcp.Required(), mcp.Description("ref can be branch/tag/commit")),
mcp.WithString("filePath", mcp.Required(), mcp.Description("file path")), mcp.WithString("filePath", mcp.Required(), mcp.Description("file path")),
mcp.WithBoolean("withLines", mcp.Description("whether to return file content with lines")),
) )
GetDirContentTool = mcp.NewTool( GetDirContentTool = mcp.NewTool(
@@ -60,7 +64,7 @@ var (
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("filePath", mcp.Required(), mcp.Description("file path")), mcp.WithString("filePath", mcp.Required(), mcp.Description("file path")),
mcp.WithString("sha", mcp.Required(), mcp.Description("sha is the SHA for the file that already exists")), mcp.WithString("sha", mcp.Required(), mcp.Description("sha is the SHA for the file that already exists")),
mcp.WithString("content", mcp.Required(), mcp.Description("file content, base64 encoded")), mcp.WithString("content", mcp.Required(), mcp.Description("file content")),
mcp.WithString("message", mcp.Required(), mcp.Description("commit message")), mcp.WithString("message", mcp.Required(), mcp.Description("commit message")),
mcp.WithString("branch_name", mcp.Required(), mcp.Description("branch name")), mcp.WithString("branch_name", mcp.Required(), mcp.Description("branch name")),
) )
@@ -100,6 +104,11 @@ func init() {
}) })
} }
type ContentLine struct {
LineNumber int `json:"line"`
Content string `json:"content"`
}
func GetFileContentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func GetFileContentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetFileFn") log.Debugf("Called GetFileFn")
owner, ok := req.GetArguments()["owner"].(string) owner, ok := req.GetArguments()["owner"].(string)
@@ -115,10 +124,52 @@ func GetFileContentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("filePath is required")) return to.ErrorResult(fmt.Errorf("filePath is required"))
} }
content, _, err := gitea.Client().GetContents(owner, repo, ref, filePath) client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
content, _, err := client.GetContents(owner, repo, ref, filePath)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("get file err: %v", err)) return to.ErrorResult(fmt.Errorf("get file err: %v", err))
} }
withLines, _ := req.GetArguments()["withLines"].(bool)
if withLines {
rawContent, err := base64.StdEncoding.DecodeString(*content.Content)
if err != nil {
return to.ErrorResult(fmt.Errorf("decode base64 content err: %v", err))
}
contentLines := make([]ContentLine, 0)
line := 0
scanner := bufio.NewScanner(bytes.NewReader(rawContent))
for scanner.Scan() {
line++
contentLines = append(contentLines, ContentLine{
LineNumber: line,
Content: scanner.Text(),
})
}
if err := scanner.Err(); err != nil {
return to.ErrorResult(fmt.Errorf("scan content err: %v", err))
}
// remove the last blank line if exists
// git does not consider the last line as a new line
if contentLines[len(contentLines)-1].Content == "" {
contentLines = contentLines[:len(contentLines)-1]
}
contentBytes, err := json.MarshalIndent(contentLines, "", " ")
if err != nil {
return to.ErrorResult(fmt.Errorf("marshal content lines err: %v", err))
}
contentStr := string(contentBytes)
content.Content = &contentStr
}
return to.TextResult(content) return to.TextResult(content)
} }
@@ -137,7 +188,11 @@ func GetDirContentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToo
if !ok { if !ok {
return to.ErrorResult(fmt.Errorf("filePath is required")) return to.ErrorResult(fmt.Errorf("filePath is required"))
} }
content, _, err := gitea.Client().ListContents(owner, repo, ref, filePath) client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
content, _, err := client.ListContents(owner, repo, ref, filePath)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("get dir content err: %v", err)) return to.ErrorResult(fmt.Errorf("get dir content err: %v", err))
} }
@@ -169,7 +224,11 @@ func CreateFileFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRe
}, },
} }
_, _, err := gitea.Client().CreateFile(owner, repo, filePath, opt) client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, _, err = client.CreateFile(owner, repo, filePath, opt)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("create file err: %v", err)) return to.ErrorResult(fmt.Errorf("create file err: %v", err))
} }
@@ -206,7 +265,11 @@ func UpdateFileFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRe
BranchName: branchName, BranchName: branchName,
}, },
} }
_, _, err := gitea.Client().UpdateFile(owner, repo, filePath, opt) client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, _, err = client.UpdateFile(owner, repo, filePath, opt)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("update file err: %v", err)) return to.ErrorResult(fmt.Errorf("update file err: %v", err))
} }
@@ -240,7 +303,11 @@ func DeleteFileFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRe
}, },
SHA: sha, SHA: sha,
} }
_, err := gitea.Client().DeleteFile(owner, repo, filePath, opt) client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.DeleteFile(owner, repo, filePath, opt)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("delete file err: %v", err)) return to.ErrorResult(fmt.Errorf("delete file err: %v", err))
} }

View File

@@ -34,6 +34,7 @@ var (
mcp.WithString("title", mcp.Required(), mcp.Description("release title")), mcp.WithString("title", mcp.Required(), mcp.Description("release title")),
mcp.WithBoolean("is_draft", mcp.Description("Whether the release is draft"), mcp.DefaultBool(false)), mcp.WithBoolean("is_draft", mcp.Description("Whether the release is draft"), mcp.DefaultBool(false)),
mcp.WithBoolean("is_pre_release", mcp.Description("Whether the release is pre-release"), mcp.DefaultBool(false)), mcp.WithBoolean("is_pre_release", mcp.Description("Whether the release is pre-release"), mcp.DefaultBool(false)),
mcp.WithString("body", mcp.Description("release body")),
) )
DeleteReleaseTool = mcp.NewTool( DeleteReleaseTool = mcp.NewTool(
@@ -131,11 +132,17 @@ func CreateReleaseFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToo
} }
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)
body, _ := req.GetArguments()["body"].(string)
_, _, err := gitea.Client().CreateRelease(owner, repo, gitea_sdk.CreateReleaseOption{ client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, _, err = client.CreateRelease(owner, repo, gitea_sdk.CreateReleaseOption{
TagName: tagName, TagName: tagName,
Target: target, Target: target,
Title: title, Title: title,
Note: body,
IsDraft: isDraft, IsDraft: isDraft,
IsPrerelease: isPreRelease, IsPrerelease: isPreRelease,
}) })
@@ -161,7 +168,11 @@ func DeleteReleaseFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToo
return nil, fmt.Errorf("id is required") return nil, fmt.Errorf("id is required")
} }
_, err := gitea.Client().DeleteRelease(owner, repo, int64(id)) client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, 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)
} }
@@ -184,7 +195,11 @@ func GetReleaseFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRe
return nil, fmt.Errorf("id is required") return nil, fmt.Errorf("id is required")
} }
release, _, err := gitea.Client().GetRelease(owner, repo, int64(id)) client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
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)
} }
@@ -203,7 +218,11 @@ func GetLatestReleaseFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call
return nil, fmt.Errorf("repo is required") return nil, fmt.Errorf("repo is required")
} }
release, _, err := gitea.Client().GetLatestRelease(owner, repo) client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
release, _, err := client.GetLatestRelease(owner, repo)
if err != nil { if err != nil {
return nil, fmt.Errorf("get latest release error: %v", err) return nil, fmt.Errorf("get latest release error: %v", err)
} }
@@ -234,7 +253,11 @@ func ListReleasesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTool
page, _ := req.GetArguments()["page"].(float64) page, _ := req.GetArguments()["page"].(float64)
pageSize, _ := req.GetArguments()["pageSize"].(float64) pageSize, _ := req.GetArguments()["pageSize"].(float64)
releases, _, err := gitea.Client().ListReleases(owner, repo, gitea_sdk.ListReleasesOptions{ client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
releases, _, err := client.ListReleases(owner, repo, gitea_sdk.ListReleasesOptions{
ListOptions: gitea_sdk.ListOptions{ ListOptions: gitea_sdk.ListOptions{
Page: int(page), Page: int(page),
PageSize: int(pageSize), PageSize: int(pageSize),

View File

@@ -27,7 +27,7 @@ const (
var ( var (
CreateRepoTool = mcp.NewTool( CreateRepoTool = mcp.NewTool(
CreateRepoToolName, CreateRepoToolName,
mcp.WithDescription("Create repository"), mcp.WithDescription("Create repository in personal account or organization"),
mcp.WithString("name", mcp.Required(), mcp.Description("Name of the repository to create")), mcp.WithString("name", mcp.Required(), mcp.Description("Name of the repository to create")),
mcp.WithString("description", mcp.Description("Description of the repository to create")), mcp.WithString("description", mcp.Description("Description of the repository to create")),
mcp.WithBoolean("private", mcp.Description("Whether the repository is private")), mcp.WithBoolean("private", mcp.Description("Whether the repository is private")),
@@ -38,6 +38,7 @@ var (
mcp.WithString("license", mcp.Description("License to use")), mcp.WithString("license", mcp.Description("License to use")),
mcp.WithString("readme", mcp.Description("Readme of the repository to create")), mcp.WithString("readme", mcp.Description("Readme of the repository to create")),
mcp.WithString("default_branch", mcp.Description("DefaultBranch of the repository (used when initializes and in template)")), mcp.WithString("default_branch", mcp.Description("DefaultBranch of the repository (used when initializes and in template)")),
mcp.WithString("organization", mcp.Description("Organization name to create repository in (optional - defaults to personal account)")),
) )
ForkRepoTool = mcp.NewTool( ForkRepoTool = mcp.NewTool(
@@ -120,6 +121,7 @@ func CreateRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRe
license, _ := req.GetArguments()["license"].(string) license, _ := req.GetArguments()["license"].(string)
readme, _ := req.GetArguments()["readme"].(string) readme, _ := req.GetArguments()["readme"].(string)
defaultBranch, _ := req.GetArguments()["default_branch"].(string) defaultBranch, _ := req.GetArguments()["default_branch"].(string)
organization, _ := req.GetArguments()["organization"].(string)
opt := gitea_sdk.CreateRepoOption{ opt := gitea_sdk.CreateRepoOption{
Name: name, Name: name,
@@ -133,9 +135,22 @@ func CreateRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRe
Readme: readme, Readme: readme,
DefaultBranch: defaultBranch, DefaultBranch: defaultBranch,
} }
repo, _, err := gitea.Client().CreateRepo(opt)
var repo *gitea_sdk.Repository
client, err := gitea.ClientFromContext(ctx)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("create repo err: %v", err)) return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
if organization != "" {
repo, _, err = client.CreateOrgRepo(organization, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("create organization repository '%s' in '%s' err: %v", name, organization, err))
}
} else {
repo, _, err = client.CreateRepo(opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("create repository '%s' err: %v", name, err))
}
} }
return to.TextResult(repo) return to.TextResult(repo)
} }
@@ -164,7 +179,11 @@ func ForkRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResu
Organization: organizationPtr, Organization: organizationPtr,
Name: namePtr, Name: namePtr,
} }
_, _, err := gitea.Client().CreateFork(user, repo, opt) client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, _, err = client.CreateFork(user, repo, opt)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("fork repository error: %v", err)) return to.ErrorResult(fmt.Errorf("fork repository error: %v", err))
} }
@@ -187,7 +206,11 @@ func ListMyReposFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolR
PageSize: int(pageSize), PageSize: int(pageSize),
}, },
} }
repos, _, err := gitea.Client().ListMyRepos(opt) client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
repos, _, err := client.ListMyRepos(opt)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("list my repositories error: %v", err)) return to.ErrorResult(fmt.Errorf("list my repositories error: %v", err))
} }

View File

@@ -102,7 +102,11 @@ func CreateTagFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRes
target, _ := req.GetArguments()["target"].(string) target, _ := req.GetArguments()["target"].(string)
message, _ := req.GetArguments()["message"].(string) message, _ := req.GetArguments()["message"].(string)
_, _, err := gitea.Client().CreateTag(owner, repo, gitea_sdk.CreateTagOption{ client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, _, err = client.CreateTag(owner, repo, gitea_sdk.CreateTagOption{
TagName: tagName, TagName: tagName,
Target: target, Target: target,
Message: message, Message: message,
@@ -129,7 +133,11 @@ func DeleteTagFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRes
return nil, fmt.Errorf("tag_name is required") return nil, fmt.Errorf("tag_name is required")
} }
_, err := gitea.Client().DeleteTag(owner, repo, tagName) client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.DeleteTag(owner, repo, tagName)
if err != nil { if err != nil {
return nil, fmt.Errorf("delete tag error: %v", err) return nil, fmt.Errorf("delete tag error: %v", err)
} }
@@ -152,7 +160,11 @@ func GetTagFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult
return nil, fmt.Errorf("tag_name is required") return nil, fmt.Errorf("tag_name is required")
} }
tag, _, err := gitea.Client().GetTag(owner, repo, tagName) client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
tag, _, err := client.GetTag(owner, repo, tagName)
if err != nil { if err != nil {
return nil, fmt.Errorf("get tag error: %v", err) return nil, fmt.Errorf("get tag error: %v", err)
} }
@@ -173,7 +185,11 @@ func ListTagsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResu
page, _ := req.GetArguments()["page"].(float64) page, _ := req.GetArguments()["page"].(float64)
pageSize, _ := req.GetArguments()["pageSize"].(float64) pageSize, _ := req.GetArguments()["pageSize"].(float64)
tags, _, err := gitea.Client().ListRepoTags(owner, repo, gitea_sdk.ListRepoTagsOptions{ client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
tags, _, err := client.ListRepoTags(owner, repo, gitea_sdk.ListRepoTagsOptions{
ListOptions: gitea_sdk.ListOptions{ ListOptions: gitea_sdk.ListOptions{
Page: int(page), Page: int(page),
PageSize: int(pageSize), PageSize: int(pageSize),

View File

@@ -94,7 +94,11 @@ func SearchUsersFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolR
PageSize: int(pageSize), PageSize: int(pageSize),
}, },
} }
users, _, err := gitea.Client().SearchUsers(opt) client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
users, _, err := client.SearchUsers(opt)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("search users err: %v", err)) return to.ErrorResult(fmt.Errorf("search users err: %v", err))
} }
@@ -128,7 +132,11 @@ func SearchOrgTeamsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTo
PageSize: int(pageSize), PageSize: int(pageSize),
}, },
} }
teams, _, err := gitea.Client().SearchOrgTeams(org, &opt) client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
teams, _, err := client.SearchOrgTeams(org, &opt)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("search organization teams error: %v", err)) return to.ErrorResult(fmt.Errorf("search organization teams error: %v", err))
} }
@@ -178,7 +186,11 @@ func SearchReposFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolR
PageSize: int(pageSize), PageSize: int(pageSize),
}, },
} }
repos, _, err := gitea.Client().SearchRepos(opt) client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
repos, _, err := client.SearchRepos(opt)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("search repos error: %v", err)) return to.ErrorResult(fmt.Errorf("search repos error: %v", err))
} }

View File

@@ -15,68 +15,102 @@ import (
) )
const ( const (
// GetMyUserInfoToolName is the unique tool name used for MCP registration and lookup of the get_my_user_info command.
GetMyUserInfoToolName = "get_my_user_info" GetMyUserInfoToolName = "get_my_user_info"
GetUserOrgsToolName = "get_user_orgs" // GetUserOrgsToolName is the unique tool name used for MCP registration and lookup of the get_user_orgs command.
GetUserOrgsToolName = "get_user_orgs"
// defaultPage is the default starting page number used for paginated organization listings.
defaultPage = 1
// defaultPageSize is the default number of organizations per page for paginated queries.
defaultPageSize = 100
) )
// Tool is the MCP tool manager instance for registering all MCP tools in this package.
var Tool = tool.New() var Tool = tool.New()
var ( var (
// GetMyUserInfoTool is the MCP tool for retrieving the current user's info.
// It is registered with a specific name and a description string.
GetMyUserInfoTool = mcp.NewTool( GetMyUserInfoTool = mcp.NewTool(
GetMyUserInfoToolName, GetMyUserInfoToolName,
mcp.WithDescription("Get my user info"), mcp.WithDescription("Get my user info"),
) )
// GetUserOrgsTool is the MCP tool for listing organizations for the authenticated user.
// It supports pagination via "page" and "pageSize" arguments with default values specified above.
GetUserOrgsTool = mcp.NewTool( GetUserOrgsTool = mcp.NewTool(
GetUserOrgsToolName, GetUserOrgsToolName,
mcp.WithDescription("Get organizations associated with the authenticated user"), mcp.WithDescription("Get organizations associated with the authenticated user"),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)), mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(defaultPage)),
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)), mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(defaultPageSize)),
) )
) )
// init registers all MCP tools in Tool at package initialization.
// This function ensures the handler functions are registered before server usage.
func init() { func init() {
Tool.RegisterRead(server.ServerTool{ registerTools()
Tool: GetMyUserInfoTool,
Handler: GetUserInfoFn,
})
Tool.RegisterRead(server.ServerTool{
Tool: GetUserOrgsTool,
Handler: GetUserOrgsFn,
})
} }
// registerTools registers all local MCP tool definitions and their handler functions.
// To add new functionality, append your tool/handler pair to the tools slice below.
func registerTools() {
tools := []server.ServerTool{
{Tool: GetMyUserInfoTool, Handler: GetUserInfoFn},
{Tool: GetUserOrgsTool, Handler: GetUserOrgsFn},
}
for _, t := range tools {
Tool.RegisterRead(t)
}
}
// 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.
func getIntArg(req mcp.CallToolRequest, name string, def int) int {
val, ok := req.GetArguments()[name].(float64)
if !ok || val < 1 {
return def
}
return int(val)
}
// GetUserInfoFn is the handler for "get_my_user_info" MCP tool requests.
// Logs invocation, fetches current user info from gitea, wraps result for MCP.
func GetUserInfoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func GetUserInfoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetUserInfoFn") log.Debugf("[User] Called GetUserInfoFn")
user, _, err := gitea.Client().GetMyUserInfo() client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
user, _, err := client.GetMyUserInfo()
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("get user info err: %v", err)) return to.ErrorResult(fmt.Errorf("get user info err: %v", err))
} }
return to.TextResult(user) return to.TextResult(user)
} }
// GetUserOrgsFn is the handler for "get_user_orgs" MCP tool requests.
// Logs invocation, pulls validated pagination arguments from request,
// performs Gitea organization listing, and wraps the result for MCP.
func GetUserOrgsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { func GetUserOrgsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetUserOrgsFn") log.Debugf("[User] Called GetUserOrgsFn")
page, ok := req.GetArguments()["page"].(float64) page := getIntArg(req, "page", defaultPage)
if !ok || page < 1 { pageSize := getIntArg(req, "pageSize", defaultPageSize)
page = 1
}
pageSize, ok := req.GetArguments()["pageSize"].(float64)
if !ok || pageSize < 1 {
pageSize = 100
}
opt := gitea_sdk.ListOrgsOptions{ opt := gitea_sdk.ListOrgsOptions{
ListOptions: gitea_sdk.ListOptions{ ListOptions: gitea_sdk.ListOptions{
Page: int(page), Page: page,
PageSize: int(pageSize), PageSize: pageSize,
}, },
} }
orgs, _, err := gitea.Client().ListMyOrgs(opt) client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
orgs, _, err := client.ListMyOrgs(opt)
if err != nil { if err != nil {
return to.ErrorResult(fmt.Errorf("get user orgs err: %v", err)) return to.ErrorResult(fmt.Errorf("get user orgs err: %v", err))
} }
return to.TextResult(orgs) return to.TextResult(orgs)
} }

7
pkg/context/context.go Normal file
View File

@@ -0,0 +1,7 @@
package context
type contextKey string
const (
TokenContextKey = contextKey("token")
)

View File

@@ -1,48 +1,47 @@
package gitea package gitea
import ( import (
"context"
"crypto/tls" "crypto/tls"
"fmt"
"net/http" "net/http"
"sync"
"gitea.com/gitea/gitea-mcp/pkg/flag"
"gitea.com/gitea/gitea-mcp/pkg/log"
"code.gitea.io/sdk/gitea" "code.gitea.io/sdk/gitea"
mcpContext "gitea.com/gitea/gitea-mcp/pkg/context"
"gitea.com/gitea/gitea-mcp/pkg/flag"
) )
var ( func NewClient(token string) (*gitea.Client, error) {
client *gitea.Client httpClient := &http.Client{
clientOnce sync.Once Transport: http.DefaultTransport,
) }
func Client() *gitea.Client { opts := []gitea.ClientOption{
clientOnce.Do(func() { gitea.SetToken(token),
var err error }
if client != nil { if flag.Insecure {
return httpClient.Transport.(*http.Transport).TLSClientConfig = &tls.Config{
InsecureSkipVerify: true,
} }
}
opts = append(opts, gitea.SetHTTPClient(httpClient))
if flag.Debug {
opts = append(opts, gitea.SetDebugMode())
}
client, err := gitea.NewClient(flag.Host, opts...)
if err != nil {
return nil, fmt.Errorf("create gitea client err: %w", err)
}
httpClient := &http.Client{ // Set user agent for the client
Transport: http.DefaultTransport, client.SetUserAgent(fmt.Sprintf("gitea-mcp-server/%s", flag.Version))
} return client, nil
}
opts := []gitea.ClientOption{
gitea.SetToken(flag.Token), func ClientFromContext(ctx context.Context) (*gitea.Client, error) {
} token, ok := ctx.Value(mcpContext.TokenContextKey).(string)
if flag.Insecure { if !ok {
httpClient.Transport.(*http.Transport).TLSClientConfig = &tls.Config{ token = flag.Token
InsecureSkipVerify: true, }
} return NewClient(token)
}
opts = append(opts, gitea.SetHTTPClient(httpClient))
if flag.Debug {
opts = append(opts, gitea.SetDebugMode())
}
client, err = gitea.NewClient(flag.Host, opts...)
if err != nil {
log.Fatalf("create gitea client err: %v", err)
}
})
return client
} }