Compare commits

...
Author SHA1 Message Date
gronod 026f40bc15 feat(issue): add dependency methods to issue tools
check-and-test / check-and-test (pull_request) Successful in 2m21s
Expose Gitea /issues/{i}/dependencies endpoints: list_dependencies on
issue_read and add_dependency/remove_dependency on issue_write via the
blocking_issue param.

Assisted-by: Devin:SWE-2 Max
2026-09-15 14:48:10 +01:00
gronod 4f7a191b67 feat(issue): add block/unblock methods to issue tools
check-and-test / check-and-test (pull_request) Successful in 2m46s
Expose Gitea /issues/{i}/blocks endpoints: list_blocks on issue_read and
block_issue/unblock_issue on issue_write, with pagination params on
issue_read.

Assisted-by: Devin:SWE-2 Max
2026-09-15 14:46:47 +01:00
Renovate Bot 87fd1a7fa2 chore(deps): update module golang.org/x/vuln to v1.8.0 (#267)
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
2026-09-14 12:06:52 +00:00
Renovate Bot e31f3c3764 chore(deps): update dependencies (#265)
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
2026-09-08 01:32:47 +00:00
Renovate Bot 0157291800 fix(deps): update module golang.org/x/crypto to v0.56.0 [security] (#262)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) | [`v0.55.0` → `v0.56.0`](https://cs.opensource.google/go/x/crypto/+/refs/tags/v0.55.0...refs/tags/v0.56.0) | ![age](https://developer.mend.io/api/mc/badges/age/go/golang.org%2fx%2fcrypto/v0.56.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/golang.org%2fx%2fcrypto/v0.55.0/v0.56.0?slim=true) |

---

### Prevent DoS on deadlocked undecided channel in golang.org/x/crypto/ssh
[CVE-2026-78662](https://nvd.nist.gov/vuln/detail/CVE-2026-78662) / [GO-2026-6354](https://pkg.go.dev/vuln/GO-2026-6354)

<details>
<summary>More information</summary>

#### Details
Previously, a channel registered in the mux's chanList is not usable until it is established. A malicious peer was able flood the channel's incomingRequests, deadlocking the entire connection.

Now, we add an atomic established state, set when a channel becomes usable. Until such a time, handlePacket drops every packet other than the open confirmation/failure, without blocking and without tearing down the connection.

#### Severity
Unknown

#### References
- [https://go.dev/issue/81316](https://go.dev/issue/81316)
- [https://go.dev/cl/826504](https://go.dev/cl/826504)
- [https://groups.google.com/g/golang-announce/c/1y3fb2np35U](https://groups.google.com/g/golang-announce/c/1y3fb2np35U)

This data is provided by [OSV](https://osv.dev/vulnerability/GO-2026-6354) and the [Go Vulnerability Database](https://github.com/golang/vulndb) ([CC-BY 4.0](https://github.com/golang/vulndb#license)).
</details>

---

### Prevent DoS on deadlocked established channel in golang.org/x/crypto/ssh
[CVE-2026-56855](https://nvd.nist.gov/vuln/detail/CVE-2026-56855) / [GO-2026-6355](https://pkg.go.dev/vuln/GO-2026-6355)

<details>
<summary>More information</summary>

#### Details
Previously, after a channel has been established, a malicious peer could send crafted messages that would deadlock the entire connection.

Now, we handle all RFC 4254 channel messages; global requests are handled explicitly. Then, treat all other messages as a protocol error and tear the connection down instead of buffering and blocking.

#### Severity
Unknown

#### References
- [https://go.dev/issue/81317](https://go.dev/issue/81317)
- [https://go.dev/cl/826524](https://go.dev/cl/826524)
- [https://groups.google.com/g/golang-announce/c/1y3fb2np35U](https://groups.google.com/g/golang-announce/c/1y3fb2np35U)

This data is provided by [OSV](https://osv.dev/vulnerability/GO-2026-6355) and the [Go Vulnerability Database](https://github.com/golang/vulndb) ([CC-BY 4.0](https://github.com/golang/vulndb#license)).
</details>

---

This PR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xOTEuMiIsInVwZGF0ZWRJblZlciI6IjQzLjE5MS4yIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Reviewed-on: https://gitea.com/gitea/gitea-mcp/pulls/262
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
2026-09-03 06:41:19 +00:00
12 changed files with 316 additions and 19 deletions
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
fetch-depth: 0 # all history for all branches and tags fetch-depth: 0 # all history for all branches and tags
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4 uses: docker/setup-qemu-action@1f40c72289eff860ee54a304f1438e3cff362e0a # v4
- name: Set up Docker BuildX - name: Set up Docker BuildX
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4 uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4
+1 -1
View File
@@ -63,7 +63,7 @@ jobs:
fetch-depth: 0 # all history for all branches and tags fetch-depth: 0 # all history for all branches and tags
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4 uses: docker/setup-qemu-action@1f40c72289eff860ee54a304f1438e3cff362e0a # v4
- name: Set up Docker BuildX - name: Set up Docker BuildX
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4 uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4
+1 -1
View File
@@ -1,4 +1,4 @@
# syntax=docker/dockerfile:1.26 # syntax=docker/dockerfile:1.27
# Build stage # Build stage
FROM --platform=$BUILDPLATFORM golang:1.27-alpine AS builder FROM --platform=$BUILDPLATFORM golang:1.27-alpine AS builder
+2 -2
View File
@@ -3,8 +3,8 @@ 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.13.1 # renovate: datasource=go GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.13.2 # renovate: datasource=go
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1.7.0 # renovate: datasource=go GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1.8.0 # renovate: datasource=go
GOTEST_FLAGS ?= -race -timeout 20m GOTEST_FLAGS ?= -race -timeout 20m
+2 -2
View File
@@ -160,8 +160,8 @@ Once configured, try `list all my repositories` in the chat box.
| package_write | packages | Write | Delete a package version (irreversible) | | package_write | packages | Write | Delete a package version (irreversible) |
| list_issues | issue | Read | List repository issues | | list_issues | issue | Read | List repository issues |
| attachment_read | issue | Read | Read issue/comment attachments: list metadata, get metadata, or download content | | attachment_read | issue | Read | Read issue/comment attachments: list metadata, get metadata, or download content |
| issue_read | issue | Read | Read issue: details, comments, or labels | | issue_read | issue | Read | Read issue: details, comments, labels, or blocking relationships |
| issue_write | issue | Write | Write issues: create, update, manage comments and labels | | issue_write | issue | Write | Write issues: create, update, manage comments, labels, and blocking relationships |
| list_pull_requests | pull_request | Read | List repository pull requests | | list_pull_requests | pull_request | Read | List repository pull requests |
| pull_request_read | pull_request | Read | Read pull request: details, diff, files, status, reviews, review comments | | pull_request_read | pull_request | Read | Read pull request: details, diff, files, status, reviews, review comments |
| pull_request_write | pull_request | Write | Write pull requests: create, update, close, reopen, merge, update branch, manage reviewers | | pull_request_write | pull_request | Write | Write pull requests: create, update, close, reopen, merge, update branch, manage reviewers |
+2 -2
View File
@@ -160,8 +160,8 @@ Cursor 等客户端可使用 stdio 命令:
| package_write | packages | 写入 | 删除软件包版本(不可恢复) | | package_write | packages | 写入 | 删除软件包版本(不可恢复) |
| list_issues | issue | 读取 | 列出仓库问题 | | list_issues | issue | 读取 | 列出仓库问题 |
| attachment_read | issue | 读取 | 读取问题/评论附件:列出元数据、获取元数据或下载内容 | | attachment_read | issue | 读取 | 读取问题/评论附件:列出元数据、获取元数据或下载内容 |
| issue_read | issue | 读取 | 读取问题:详情、评论或标签 | | issue_read | issue | 读取 | 读取问题:详情、评论、标签或阻塞关系 |
| issue_write | issue | 写入 | 写入问题:创建、更新、管理评论和标签 | | issue_write | issue | 写入 | 写入问题:创建、更新、管理评论、标签和阻塞关系 |
| list_pull_requests | pull_request | 读取 | 列出仓库拉取请求 | | list_pull_requests | pull_request | 读取 | 列出仓库拉取请求 |
| pull_request_read | pull_request | 读取 | 读取拉取请求:详情、差异、变更文件、头部提交状态、审查、审查评论 | | pull_request_read | pull_request | 读取 | 读取拉取请求:详情、差异、变更文件、头部提交状态、审查、审查评论 |
| pull_request_write | pull_request | 写入 | 写入拉取请求:创建、更新、关闭、重新打开、合并、更新分支、管理审查者 | | pull_request_write | pull_request | 写入 | 写入拉取请求:创建、更新、关闭、重新打开、合并、更新分支、管理审查者 |
+2 -2
View File
@@ -160,8 +160,8 @@ Cursor 等客戶端可使用 stdio 命令:
| package_write | packages | 寫入 | 刪除軟體套件版本(不可復原) | | package_write | packages | 寫入 | 刪除軟體套件版本(不可復原) |
| list_issues | issue | 讀取 | 列出倉庫問題 | | list_issues | issue | 讀取 | 列出倉庫問題 |
| attachment_read | issue | 讀取 | 讀取問題/評論附件:列出中繼資料、取得中繼資料或下載內容 | | attachment_read | issue | 讀取 | 讀取問題/評論附件:列出中繼資料、取得中繼資料或下載內容 |
| issue_read | issue | 讀取 | 讀取問題:詳情、評論或標籤 | | issue_read | issue | 讀取 | 讀取問題:詳情、評論、標籤或阻塞關係 |
| issue_write | issue | 寫入 | 寫入問題:創建、更新、管理評論和標籤 | | issue_write | issue | 寫入 | 寫入問題:創建、更新、管理評論、標籤和阻塞關係 |
| list_pull_requests | pull_request | 讀取 | 列出倉庫拉取請求 | | list_pull_requests | pull_request | 讀取 | 列出倉庫拉取請求 |
| pull_request_read | pull_request | 讀取 | 讀取拉取請求:詳情、差異、變更檔案、頭部提交狀態、審查、審查評論 | | pull_request_read | pull_request | 讀取 | 讀取拉取請求:詳情、差異、變更檔案、頭部提交狀態、審查、審查評論 |
| pull_request_write | pull_request | 寫入 | 寫入拉取請求:創建、更新、關閉、重新開啟、合併、更新分支、管理審查者 | | pull_request_write | pull_request | 寫入 | 寫入拉取請求:創建、更新、關閉、重新開啟、合併、更新分支、管理審查者 |
+2 -2
View File
@@ -2,7 +2,7 @@ module gitea.com/gitea/gitea-mcp
go 1.27 go 1.27
toolchain go1.27.0 toolchain go1.27.1
require ( require (
gitea.dev/sdk v1.2.0 gitea.dev/sdk v1.2.0
@@ -21,7 +21,7 @@ require (
github.com/segmentio/encoding v0.5.4 // indirect github.com/segmentio/encoding v0.5.4 // 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.55.0 // indirect golang.org/x/crypto v0.56.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sync v0.22.0 // indirect golang.org/x/sync v0.22.0 // indirect
golang.org/x/sys v0.47.0 // indirect golang.org/x/sys v0.47.0 // indirect
+2 -2
View File
@@ -37,8 +37,8 @@ go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
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.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y=
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I=
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/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
+134
View File
@@ -0,0 +1,134 @@
package issue
import (
"context"
"fmt"
"gitea.com/gitea/gitea-mcp/pkg/gitea"
"gitea.com/gitea/gitea-mcp/pkg/params"
"gitea.com/gitea/gitea-mcp/pkg/to"
gitea_sdk "gitea.dev/sdk"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
func listIssueBlocksFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) {
owner, repo, index, err := issueRef(args)
if err != nil {
return to.ErrorResult(err)
}
page, pageSize := params.GetPagination(args, 30)
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
listOpt := gitea_sdk.ListOptions{Page: page, PageSize: pageSize}
issues, _, err := client.Issues.ListIssueBlocks(ctx, owner, repo, index, gitea_sdk.ListIssueBlocksOptions{
ListOptions: listOpt,
})
if err != nil {
return to.ErrorResult(fmt.Errorf("list %v/%v/issue/%v/blocks err: %v", owner, repo, index, err))
}
return to.TextResult(slimIssues(issues))
}
func blockIssueFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) {
return setIssueBlockingFn(ctx, args, true)
}
func unblockIssueFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) {
return setIssueBlockingFn(ctx, args, false)
}
func setIssueBlockingFn(ctx context.Context, args map[string]any, block bool) (*mcp.CallToolResult, error) {
owner, repo, index, err := issueRef(args)
if err != nil {
return to.ErrorResult(err)
}
blocked, err := params.GetIndex(args, "blocked_issue")
if err != nil {
return to.ErrorResult(err)
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
meta := gitea_sdk.IssueMeta{Index: blocked}
var issue *gitea_sdk.Issue
if block {
issue, _, err = client.Issues.CreateIssueBlocking(ctx, owner, repo, index, meta)
} else {
issue, _, err = client.Issues.RemoveIssueBlocking(ctx, owner, repo, index, meta)
}
if err != nil {
return to.ErrorResult(fmt.Errorf("update %v/%v/issue/%v/blocks err: %v", owner, repo, index, err))
}
return to.TextResult(slimIssue(issue))
}
func listIssueDependenciesFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) {
owner, repo, index, err := issueRef(args)
if err != nil {
return to.ErrorResult(err)
}
page, pageSize := params.GetPagination(args, 30)
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
listOpt := gitea_sdk.ListOptions{Page: page, PageSize: pageSize}
issues, _, err := client.Issues.ListIssueDependencies(ctx, owner, repo, index, gitea_sdk.ListIssueDependenciesOptions{
ListOptions: listOpt,
})
if err != nil {
return to.ErrorResult(fmt.Errorf("list %v/%v/issue/%v/dependencies err: %v", owner, repo, index, err))
}
return to.TextResult(slimIssues(issues))
}
func addIssueDependencyFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) {
return setIssueDependencyFn(ctx, args, true)
}
func removeIssueDependencyFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult, error) {
return setIssueDependencyFn(ctx, args, false)
}
func setIssueDependencyFn(ctx context.Context, args map[string]any, add bool) (*mcp.CallToolResult, error) {
owner, repo, index, err := issueRef(args)
if err != nil {
return to.ErrorResult(err)
}
blocking, err := params.GetIndex(args, "blocking_issue")
if err != nil {
return to.ErrorResult(err)
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
meta := gitea_sdk.IssueMeta{Index: blocking}
var issue *gitea_sdk.Issue
if add {
issue, _, err = client.Issues.CreateIssueDependency(ctx, owner, repo, index, meta)
} else {
issue, _, err = client.Issues.RemoveIssueDependency(ctx, owner, repo, index, meta)
}
if err != nil {
return to.ErrorResult(fmt.Errorf("update %v/%v/issue/%v/dependencies err: %v", owner, repo, index, err))
}
return to.TextResult(slimIssue(issue))
}
func issueRef(args map[string]any) (owner, repo string, index int64, err error) {
if owner, err = params.GetString(args, "owner"); err != nil {
return "", "", 0, err
}
if repo, err = params.GetString(args, "repo"); err != nil {
return "", "", 0, err
}
if index, err = params.GetIndex(args, "issue_number"); err != nil {
return "", "", 0, err
}
return owner, repo, index, nil
}
+147
View File
@@ -0,0 +1,147 @@
package issue
import (
"context"
"encoding/json"
"fmt"
"maps"
"net/http"
"net/http/httptest"
"strings"
"testing"
"gitea.com/gitea/gitea-mcp/pkg/flag"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
func Test_issueBlocking(t *testing.T) {
const (
owner = "octo"
repo = "demo"
)
blocksPath := fmt.Sprintf("/api/v1/repos/%s/%s/issues/42/blocks", owner, repo)
depsPath := fmt.Sprintf("/api/v1/repos/%s/%s/issues/42/dependencies", owner, repo)
tests := []struct {
name string
fn func(context.Context, map[string]any) (*mcp.CallToolResult, error)
args map[string]any
wantPath string
wantMethod string
wantQuery string
wantIndex float64
}{
{
name: "list blocks paginates",
fn: listIssueBlocksFn,
args: map[string]any{"page": float64(2), "per_page": float64(10)},
wantPath: blocksPath,
wantMethod: http.MethodGet,
wantQuery: "limit=10&page=2",
},
{
name: "block issue posts index",
fn: blockIssueFn,
args: map[string]any{"blocked_issue": float64(7)},
wantPath: blocksPath,
wantMethod: http.MethodPost,
wantIndex: 7,
},
{
name: "unblock issue deletes with index",
fn: unblockIssueFn,
args: map[string]any{"blocked_issue": float64(7)},
wantPath: blocksPath,
wantMethod: http.MethodDelete,
wantIndex: 7,
},
{
name: "list dependencies paginates",
fn: listIssueDependenciesFn,
args: map[string]any{"page": float64(3), "per_page": float64(5)},
wantPath: depsPath,
wantMethod: http.MethodGet,
wantQuery: "limit=5&page=3",
},
{
name: "add dependency posts index",
fn: addIssueDependencyFn,
args: map[string]any{"blocking_issue": float64(9)},
wantPath: depsPath,
wantMethod: http.MethodPost,
wantIndex: 9,
},
{
name: "remove dependency deletes with index",
fn: removeIssueDependencyFn,
args: map[string]any{"blocking_issue": float64(9)},
wantPath: depsPath,
wantMethod: http.MethodDelete,
wantIndex: 9,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var (
gotMethod string
gotPath string
gotQuery 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.27.2"}`))
case blocksPath, depsPath:
gotMethod = r.Method
gotPath = r.URL.Path
gotQuery = r.URL.RawQuery
_ = json.NewDecoder(r.Body).Decode(&gotBody)
w.Header().Set("Content-Type", "application/json")
if r.Method == http.MethodGet {
_, _ = w.Write([]byte(`[{"number":7,"title":"blocked","state":"open"}]`))
} else {
_, _ = w.Write([]byte(`{"number":7,"title":"blocked","state":"open"}`))
}
default:
http.NotFound(w, r)
}
})
server := httptest.NewServer(handler)
defer server.Close()
origHost, origToken, origVersion := flag.Host, flag.Token, flag.Version
flag.Host, flag.Token, flag.Version = server.URL, "", "test"
defer func() { flag.Host, flag.Token, flag.Version = origHost, origToken, origVersion }()
args := map[string]any{"owner": owner, "repo": repo, "issue_number": float64(42)}
maps.Copy(args, tt.args)
res, err := tt.fn(context.Background(), args)
if err != nil {
t.Fatalf("handler error = %v", err)
}
if res.IsError {
t.Fatalf("unexpected error result: %v", res.Content)
}
if gotMethod != tt.wantMethod {
t.Fatalf("method = %s, want %s", gotMethod, tt.wantMethod)
}
if gotPath != tt.wantPath {
t.Fatalf("path = %s, want %s", gotPath, tt.wantPath)
}
if tt.wantQuery != "" && gotQuery != tt.wantQuery {
t.Fatalf("query = %s, want %s", gotQuery, tt.wantQuery)
}
if tt.wantIndex != 0 && gotBody["index"] != tt.wantIndex {
t.Fatalf("body index = %v, want %v", gotBody["index"], tt.wantIndex)
}
body := res.Content[0].(*mcp.TextContent).Text
if !strings.Contains(body, `"number":7`) && !strings.Contains(body, `"number": 7`) {
t.Fatalf("expected slim issue in output, got: %s", body)
}
})
}
}
+20 -4
View File
@@ -55,19 +55,21 @@ var (
IssueReadTool = tool.NewDefinition( IssueReadTool = tool.NewDefinition(
IssueReadToolName, IssueReadToolName,
"Read issue: details, comments, or labels.", "Read issue: details, comments, labels, or blocking relationships.",
annotation.ReadOnly("Read issue details"), annotation.ReadOnly("Read issue details"),
tool.String("method", tool.Required(), tool.Enum("get", "get_comments", "get_labels")), tool.String("method", tool.Required(), tool.Enum("get", "get_comments", "get_labels", "list_blocks", "list_dependencies")),
tool.String("owner", tool.Required(), tool.Description(params.OwnerDesc)), tool.String("owner", tool.Required(), tool.Description(params.OwnerDesc)),
tool.String("repo", tool.Required(), tool.Description(params.RepoDesc)), tool.String("repo", tool.Required(), tool.Description(params.RepoDesc)),
tool.Number("issue_number", tool.Required()), tool.Number("issue_number", tool.Required()),
tool.Number("page", tool.Description(params.PageDesc), tool.Default(1)),
tool.Number("per_page", tool.Description(params.PaginationDesc), tool.Default(30)),
) )
IssueWriteTool = tool.NewDefinition( IssueWriteTool = tool.NewDefinition(
IssueWriteToolName, IssueWriteToolName,
"Write issues: create, update, manage comments and labels.", "Write issues: create, update, manage comments, labels, and blocking relationships.",
annotation.Write("Create or update issues, comments, and labels"), annotation.Write("Create or update issues, comments, and labels"),
tool.String("method", tool.Required(), tool.Enum("create", "update", "add_comment", "edit_comment", "add_labels", "remove_label", "replace_labels", "clear_labels")), tool.String("method", tool.Required(), tool.Enum("create", "update", "add_comment", "edit_comment", "add_labels", "remove_label", "replace_labels", "clear_labels", "block_issue", "unblock_issue", "add_dependency", "remove_dependency")),
tool.String("owner", tool.Required(), tool.Description(params.OwnerDesc)), tool.String("owner", tool.Required(), tool.Description(params.OwnerDesc)),
tool.String("repo", tool.Required(), tool.Description(params.RepoDesc)), tool.String("repo", tool.Required(), tool.Description(params.RepoDesc)),
tool.Number("issue_number", tool.Description("required except for 'create'")), tool.Number("issue_number", tool.Description("required except for 'create'")),
@@ -82,6 +84,8 @@ var (
tool.String("ref", tool.Description("branch to associate")), tool.String("ref", tool.Description("branch to associate")),
tool.String("deadline", tool.Description("ISO 8601")), tool.String("deadline", tool.Description("ISO 8601")),
tool.Boolean("remove_deadline"), tool.Boolean("remove_deadline"),
tool.Number("blocked_issue", tool.Description("issue number that issue_number blocks (required for 'block_issue'/'unblock_issue')")),
tool.Number("blocking_issue", tool.Description("issue number that blocks issue_number (required for 'add_dependency'/'remove_dependency')")),
) )
) )
@@ -112,6 +116,10 @@ func issueReadFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult,
return getIssueCommentsByIndexFn(ctx, args) return getIssueCommentsByIndexFn(ctx, args)
case "get_labels": case "get_labels":
return getIssueLabelsFn(ctx, args) return getIssueLabelsFn(ctx, args)
case "list_blocks":
return listIssueBlocksFn(ctx, args)
case "list_dependencies":
return listIssueDependenciesFn(ctx, args)
default: default:
return to.ErrorResult(fmt.Errorf("unknown method: %s", method)) return to.ErrorResult(fmt.Errorf("unknown method: %s", method))
} }
@@ -139,6 +147,14 @@ func issueWriteFn(ctx context.Context, args map[string]any) (*mcp.CallToolResult
return replaceIssueLabelsFn(ctx, args) return replaceIssueLabelsFn(ctx, args)
case "clear_labels": case "clear_labels":
return clearIssueLabelsFn(ctx, args) return clearIssueLabelsFn(ctx, args)
case "block_issue":
return blockIssueFn(ctx, args)
case "unblock_issue":
return unblockIssueFn(ctx, args)
case "add_dependency":
return addIssueDependencyFn(ctx, args)
case "remove_dependency":
return removeIssueDependencyFn(ctx, args)
default: default:
return to.ErrorResult(fmt.Errorf("unknown method: %s", method)) return to.ErrorResult(fmt.Errorf("unknown method: %s", method))
} }