✨ (mcp): Add updateIssue and assignIssue tools
Changes
4 files changed, +78 -4
MODIFY
README.md
+1 -1
@@ -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
@@ -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
@@ -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
@@ -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
{