gitshark

Clone repository

git clone https://gitshark.de/git/workaround/Gitshark.git
git clone git@gitshark.de:workaround/Gitshark.git

← Commits

✨ (mcp): Add updateIssue and assignIssue tools

5e10e3f97998e4e93fec2b8d31b3160be4c5e0d7 · Michael Hainz · 2026-07-21T10:50:16Z

Changes

4 files changed, +78 -4

MODIFY README.md +1 -1
diff --git a/README.md b/README.md
index 5ea983f..5490fb8 100644
--- a/README.md
+++ b/README.md
@@ -166,7 +166,7 @@
166 166 | Area | Tools |
167 167 |---|---|
168 168 | Repositories | `listRepositories`, `getRepository`, `createRepository`, `forkRepository`, `deleteRepository` |
169 -| Issues | `listIssues`, `getIssue`, `createIssue`, `updateIssueStatus`, `deleteIssue` |
169 +| Issues | `listIssues`, `getIssue`, `createIssue`, `updateIssue`, `assignIssue`, `updateIssueStatus`, `deleteIssue` |
170 170 | Merge requests | `listMergeRequests`, `getMergeRequest`, `createMergeRequest`, `mergeMergeRequest`, `closeMergeRequest`, `listMergeRequestComments`, `addMergeRequestComment` |
171 171 | User | `currentUser` |
172 172
MODIFY docs/users/mcp.md +1 -1
diff --git a/docs/users/mcp.md b/docs/users/mcp.md
index bc649ec..ae87778 100644
--- a/docs/users/mcp.md
+++ b/docs/users/mcp.md
@@ -71,7 +71,7 @@
71 71 | Repositories | `listRepositories`, `getRepository` | no (public repos) |
72 72 | | `createRepository`, `forkRepository`, `deleteRepository` | yes |
73 73 | Issues | `listIssues`, `getIssue`, `listIssueComments` | no (public repos) |
74 -| | `createIssue`, `updateIssueStatus`, `deleteIssue` | yes (owner or collaborator) |
74 +| | `createIssue`, `updateIssue`, `assignIssue`, `updateIssueStatus`, `deleteIssue` | yes (owner or collaborator) |
75 75 | | `addIssueComment` | yes (any reader of the repository) |
76 76 | Merge requests | `listMergeRequests`, `getMergeRequest`, `listMergeRequestComments` | no (public repos) |
77 77 | | `createMergeRequest`, `mergeMergeRequest`, `closeMergeRequest` | yes (owner or collaborator) |
MODIFY src/main/java/de/workaround/mcp/IssueTools.java +30 -2
diff --git a/src/main/java/de/workaround/mcp/IssueTools.java b/src/main/java/de/workaround/mcp/IssueTools.java
index a2e1d85..0773a58 100644
--- a/src/main/java/de/workaround/mcp/IssueTools.java
+++ b/src/main/java/de/workaround/mcp/IssueTools.java
@@ -18,8 +18,9 @@
18 18
19 19 /**
20 20 * MCP tools mirroring the issue REST endpoints, including free-text discussion comments. Issues are
21 - * addressed by their per-repository number. Reading follows repository visibility; creating, transitioning
22 - * and deleting require a token and ownership, while commenting only requires read access.
21 + * addressed by their per-repository number. Reading follows repository visibility; creating, editing,
22 + * assigning, transitioning and deleting require a token and ownership, while commenting only requires read
23 + * access.
23 24 */
24 25 @Singleton
25 26 public class IssueTools
@@ -64,6 +65,33 @@
64 65 return ApiModels.IssueView.of(issue);
65 66 }
66 67
68 + @Tool(description = "Edit an issue's title and description. Requires a token and ownership.")
69 + @Transactional
70 + public ApiModels.IssueView updateIssue(@ToolArg(description = "Owner username") String owner,
71 + @ToolArg(description = "Repository name") String name, @ToolArg(description = "Issue number") int number,
72 + @ToolArg(description = "New title") String title,
73 + @ToolArg(description = "New description; empty clears it", defaultValue = "") String description)
74 + {
75 + User user = principal.require();
76 + Repository repo = access.requireReadable(user, owner, name);
77 + Issue issue = requireIssue(repo, number);
78 + issues.update(user, issue, title, description == null || description.isBlank() ? null : description);
79 + return ApiModels.IssueView.of(requireIssue(repo, number));
80 + }
81 +
82 + @Tool(description = "Assign an issue to a user, or unassign it when the username is empty. Requires a token and ownership.")
83 + @Transactional
84 + public ApiModels.IssueView assignIssue(@ToolArg(description = "Owner username") String owner,
85 + @ToolArg(description = "Repository name") String name, @ToolArg(description = "Issue number") int number,
86 + @ToolArg(description = "Assignee username; empty to unassign", defaultValue = "") String username)
87 + {
88 + User user = principal.require();
89 + Repository repo = access.requireReadable(user, owner, name);
90 + Issue issue = requireIssue(repo, number);
91 + issues.assign(user, issue, username);
92 + return ApiModels.IssueView.of(requireIssue(repo, number));
93 + }
94 +
67 95 @Tool(description = "Update an issue's status. Requires a token and ownership.")
68 96 @Transactional
69 97 public ApiModels.IssueView updateIssueStatus(@ToolArg(description = "Owner username") String owner,
MODIFY src/test/java/de/workaround/mcp/McpToolsTest.java +46 -0
diff --git a/src/test/java/de/workaround/mcp/McpToolsTest.java b/src/test/java/de/workaround/mcp/McpToolsTest.java
index 7c724c1..eed151e 100644
--- a/src/test/java/de/workaround/mcp/McpToolsTest.java
+++ b/src/test/java/de/workaround/mcp/McpToolsTest.java
@@ -152,6 +152,52 @@
152 152 .thenAssertResults();
153 153 }
154 154
155 + @Test
156 + void updateIssueToolEditsTitleAndDescription()
157 + {
158 + User owner = persistUser("mcp-iupd-" + shortId());
159 + Repository repo = service.create(owner, "mcpissueupdate", Repository.Visibility.PUBLIC, null);
160 + int issueNumber = createIssue(owner, repo, "original title");
161 + String token = tokenService.create(owner, "mcp").plaintext();
162 +
163 + McpStreamableTestClient client = McpAssured.newStreamableClient()
164 + .setMcpPath("/mcp")
165 + .setAdditionalHeaders(json -> MultiMap.caseInsensitiveMultiMap().add("Authorization", "Bearer " + token))
166 + .build().connect();
167 +
168 + client.when()
169 + .toolsCall("updateIssue", Map.of("owner", owner.username, "name", "mcpissueupdate",
170 + "number", issueNumber, "title", "edited title", "description", "edited body"), response -> {
171 + assertFalse(response.isError());
172 + assertTrue(response.firstContent().asText().text().contains("edited title"));
173 + assertTrue(response.firstContent().asText().text().contains("edited body"));
174 + })
175 + .thenAssertResults();
176 + }
177 +
178 + @Test
179 + void assignIssueToolSetsTheAssignee()
180 + {
181 + User owner = persistUser("mcp-iasg-" + shortId());
182 + User assignee = persistUser("mcp-iasgee-" + shortId());
183 + Repository repo = service.create(owner, "mcpissueassign", Repository.Visibility.PUBLIC, null);
184 + int issueNumber = createIssue(owner, repo, "needs owner");
185 + String token = tokenService.create(owner, "mcp").plaintext();
186 +
187 + McpStreamableTestClient client = McpAssured.newStreamableClient()
188 + .setMcpPath("/mcp")
189 + .setAdditionalHeaders(json -> MultiMap.caseInsensitiveMultiMap().add("Authorization", "Bearer " + token))
190 + .build().connect();
191 +
192 + client.when()
193 + .toolsCall("assignIssue", Map.of("owner", owner.username, "name", "mcpissueassign",
194 + "number", issueNumber, "username", assignee.username), response -> {
195 + assertFalse(response.isError());
196 + assertTrue(response.firstContent().asText().text().contains(assignee.username));
197 + })
198 + .thenAssertResults();
199 + }
200 +
155 201 @Transactional
156 202 int createIssue(User owner, Repository repo, String title)
157 203 {

Keyboard shortcuts

?Show this help
g hGo home
EscClose dialog