Skip to content

Commit 4ada320

Browse files
cvxluocodex
andauthored
fix(tools): Support unassigning issues (#1302)
Treat an explicit null assignee as a request to clear assignment while preserving omission as no update. This is similar to how the monitor update tool works. Co-authored-by: Codex CLI Agent <noreply@openai.com>
1 parent 36bb742 commit 4ada320

6 files changed

Lines changed: 75 additions & 14 deletions

File tree

‎packages/mcp-core/src/api-client/client.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4109,7 +4109,7 @@ export class SentryApiService {
41094109
organizationSlug: string;
41104110
issueId: string;
41114111
status?: string;
4112-
assignedTo?: string;
4112+
assignedTo?: string | null;
41134113
substatus?: string;
41144114
ignoreDuration?: number;
41154115
ignoreCount?: number;
@@ -4130,7 +4130,7 @@ export class SentryApiService {
41304130
ignoreUserWindow?: number;
41314131
} = {};
41324132
if (status !== undefined) updateData.status = status;
4133-
if (assignedTo !== undefined) updateData.assignedTo = assignedTo;
4133+
if (assignedTo !== undefined) updateData.assignedTo = assignedTo ?? "";
41344134
if (substatus !== undefined) updateData.substatus = substatus;
41354135
if (ignoreDuration !== undefined)
41364136
updateData.ignoreDuration = ignoreDuration;

‎packages/mcp-core/src/schema.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,8 +177,10 @@ export const ParamIssueIgnoreMode = z
177177
export const ParamAssignedTo = z
178178
.string()
179179
.trim()
180+
.min(1)
181+
.nullable()
180182
.describe(
181-
"The assignee in format 'user:ID' or 'team:ID_OR_SLUG' where ID is numeric. Example: 'user:123456', 'team:789', or 'team:my-team-slug'. Use `execute_sentry_tool(name='whoami', arguments={})` to find your user ID.",
183+
"The assignee in format 'user:ID' or 'team:ID_OR_SLUG' where ID is numeric. Pass null to unassign the issue. Example: 'user:123456', 'team:789', or 'team:my-team-slug'. Use `execute_sentry_tool(name='whoami', arguments={})` to find your user ID.",
182184
);
183185

184186
export const ParamIgnoreDurationMinutes = z

‎packages/mcp-core/src/skillDefinitions.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,7 @@
400400
},
401401
{
402402
"name": "update_issue",
403-
"description": "Update a Sentry issue's status or assignment.\n\nUse this to resolve, reopen, assign, or ignore an issue.\n\n<examples>\n```\nupdate_issue(organizationSlug='my-org', issueId='PROJECT-123', status='resolved')\nupdate_issue(organizationSlug='my-org', issueId='PROJECT-123', assignedTo='user:123456')\nupdate_issue(organizationSlug='my-org', issueId='PROJECT-123', status='ignored')\nupdate_issue(organizationSlug='my-org', issueId='PROJECT-123', status='ignored', ignoreMode='forever')\nupdate_issue(organizationSlug='my-org', issueId='PROJECT-123', status='ignored', ignoreMode='untilOccurrenceCount', ignoreCount=100, ignoreWindowMinutes=60)\nupdate_issue(organizationSlug='my-org', issueId='PROJECT-123', status='ignored', reason='Ignoring because this is expected noise from the staging deploy')\n```\n</examples>\n\n<hints>\n- Provide `issueUrl` or `organizationSlug` + `issueId`.\n- At least one of `status` or `assignedTo` is required.\n- `assignedTo` format: `user:ID` or `team:ID_OR_SLUG`.\n- Use `execute_sentry_tool(name='whoami', arguments={})` to find your user ID for self-assignment.\n- Status values: `resolved`, `resolvedInNextRelease`, `unresolved`, `ignored`.\n- `status='ignored'` defaults to `ignoreMode='untilEscalating'`.\n- Ignore modes: `untilEscalating`, `forever`, `forDuration`, `untilOccurrenceCount`, `untilUserCount`.\n- Matching ignore inputs are `ignoreDurationMinutes`, `ignoreCount` + optional `ignoreWindowMinutes`, or `ignoreUserCount` + optional `ignoreUserWindowMinutes`.\n- To switch an already ignored issue between `untilEscalating`, `forever`, and condition-based ignore modes, first set `status='unresolved'`, then ignore it again with the new rule.\n- `reason` is optional. When provided, it will be posted as a comment on the issue's activity feed explaining why the action was taken.\n</hints>",
403+
"description": "Update a Sentry issue's status or assignment.\n\nUse this to resolve, reopen, assign, unassign, or ignore an issue.\n\n<examples>\n```\nupdate_issue(organizationSlug='my-org', issueId='PROJECT-123', status='resolved')\nupdate_issue(organizationSlug='my-org', issueId='PROJECT-123', assignedTo='user:123456')\nupdate_issue(organizationSlug='my-org', issueId='PROJECT-123', assignedTo=null)\nupdate_issue(organizationSlug='my-org', issueId='PROJECT-123', status='ignored')\nupdate_issue(organizationSlug='my-org', issueId='PROJECT-123', status='ignored', ignoreMode='forever')\nupdate_issue(organizationSlug='my-org', issueId='PROJECT-123', status='ignored', ignoreMode='untilOccurrenceCount', ignoreCount=100, ignoreWindowMinutes=60)\nupdate_issue(organizationSlug='my-org', issueId='PROJECT-123', status='ignored', reason='Ignoring because this is expected noise from the staging deploy')\n```\n</examples>\n\n<hints>\n- Provide `issueUrl` or `organizationSlug` + `issueId`.\n- At least one of `status` or `assignedTo` is required.\n- Omit `assignedTo` to leave assignment unchanged. Pass `null` to unassign; otherwise use `user:ID` or `team:ID_OR_SLUG`.\n- Use `execute_sentry_tool(name='whoami', arguments={})` to find your user ID for self-assignment.\n- Status values: `resolved`, `resolvedInNextRelease`, `unresolved`, `ignored`.\n- `status='ignored'` defaults to `ignoreMode='untilEscalating'`.\n- Ignore modes: `untilEscalating`, `forever`, `forDuration`, `untilOccurrenceCount`, `untilUserCount`.\n- Matching ignore inputs are `ignoreDurationMinutes`, `ignoreCount` + optional `ignoreWindowMinutes`, or `ignoreUserCount` + optional `ignoreUserWindowMinutes`.\n- To switch an already ignored issue between `untilEscalating`, `forever`, and condition-based ignore modes, first set `status='unresolved'`, then ignore it again with the new rule.\n- `reason` is optional. When provided, it will be posted as a comment on the issue's activity feed explaining why the action was taken.\n</hints>",
404404
"requiredScopes": ["event:write"]
405405
},
406406
{

‎packages/mcp-core/src/toolDefinitions.json‎

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5659,7 +5659,7 @@
56595659
},
56605660
{
56615661
"name": "update_issue",
5662-
"description": "Update a Sentry issue's status or assignment.\n\nUse this to resolve, reopen, assign, or ignore an issue.\n\n<examples>\n```\nupdate_issue(organizationSlug='my-org', issueId='PROJECT-123', status='resolved')\nupdate_issue(organizationSlug='my-org', issueId='PROJECT-123', assignedTo='user:123456')\nupdate_issue(organizationSlug='my-org', issueId='PROJECT-123', status='ignored')\nupdate_issue(organizationSlug='my-org', issueId='PROJECT-123', status='ignored', ignoreMode='forever')\nupdate_issue(organizationSlug='my-org', issueId='PROJECT-123', status='ignored', ignoreMode='untilOccurrenceCount', ignoreCount=100, ignoreWindowMinutes=60)\nupdate_issue(organizationSlug='my-org', issueId='PROJECT-123', status='ignored', reason='Ignoring because this is expected noise from the staging deploy')\n```\n</examples>\n\n<hints>\n- Provide `issueUrl` or `organizationSlug` + `issueId`.\n- At least one of `status` or `assignedTo` is required.\n- `assignedTo` format: `user:ID` or `team:ID_OR_SLUG`.\n- Use `execute_sentry_tool(name='whoami', arguments={})` to find your user ID for self-assignment.\n- Status values: `resolved`, `resolvedInNextRelease`, `unresolved`, `ignored`.\n- `status='ignored'` defaults to `ignoreMode='untilEscalating'`.\n- Ignore modes: `untilEscalating`, `forever`, `forDuration`, `untilOccurrenceCount`, `untilUserCount`.\n- Matching ignore inputs are `ignoreDurationMinutes`, `ignoreCount` + optional `ignoreWindowMinutes`, or `ignoreUserCount` + optional `ignoreUserWindowMinutes`.\n- To switch an already ignored issue between `untilEscalating`, `forever`, and condition-based ignore modes, first set `status='unresolved'`, then ignore it again with the new rule.\n- `reason` is optional. When provided, it will be posted as a comment on the issue's activity feed explaining why the action was taken.\n</hints>",
5662+
"description": "Update a Sentry issue's status or assignment.\n\nUse this to resolve, reopen, assign, unassign, or ignore an issue.\n\n<examples>\n```\nupdate_issue(organizationSlug='my-org', issueId='PROJECT-123', status='resolved')\nupdate_issue(organizationSlug='my-org', issueId='PROJECT-123', assignedTo='user:123456')\nupdate_issue(organizationSlug='my-org', issueId='PROJECT-123', assignedTo=null)\nupdate_issue(organizationSlug='my-org', issueId='PROJECT-123', status='ignored')\nupdate_issue(organizationSlug='my-org', issueId='PROJECT-123', status='ignored', ignoreMode='forever')\nupdate_issue(organizationSlug='my-org', issueId='PROJECT-123', status='ignored', ignoreMode='untilOccurrenceCount', ignoreCount=100, ignoreWindowMinutes=60)\nupdate_issue(organizationSlug='my-org', issueId='PROJECT-123', status='ignored', reason='Ignoring because this is expected noise from the staging deploy')\n```\n</examples>\n\n<hints>\n- Provide `issueUrl` or `organizationSlug` + `issueId`.\n- At least one of `status` or `assignedTo` is required.\n- Omit `assignedTo` to leave assignment unchanged. Pass `null` to unassign; otherwise use `user:ID` or `team:ID_OR_SLUG`.\n- Use `execute_sentry_tool(name='whoami', arguments={})` to find your user ID for self-assignment.\n- Status values: `resolved`, `resolvedInNextRelease`, `unresolved`, `ignored`.\n- `status='ignored'` defaults to `ignoreMode='untilEscalating'`.\n- Ignore modes: `untilEscalating`, `forever`, `forDuration`, `untilOccurrenceCount`, `untilUserCount`.\n- Matching ignore inputs are `ignoreDurationMinutes`, `ignoreCount` + optional `ignoreWindowMinutes`, or `ignoreUserCount` + optional `ignoreUserWindowMinutes`.\n- To switch an already ignored issue between `untilEscalating`, `forever`, and condition-based ignore modes, first set `status='unresolved'`, then ignore it again with the new rule.\n- `reason` is optional. When provided, it will be posted as a comment on the issue's activity feed explaining why the action was taken.\n</hints>",
56635663
"inputSchema": {
56645664
"type": "object",
56655665
"properties": {
@@ -5699,8 +5699,16 @@
56995699
"description": "The new status for the issue. Valid values are 'resolved', 'resolvedInNextRelease', 'unresolved', and 'ignored'."
57005700
},
57015701
"assignedTo": {
5702-
"type": "string",
5703-
"description": "The assignee in format 'user:ID' or 'team:ID_OR_SLUG' where ID is numeric. Example: 'user:123456', 'team:789', or 'team:my-team-slug'. Use `execute_sentry_tool(name='whoami', arguments={})` to find your user ID."
5702+
"anyOf": [
5703+
{
5704+
"type": "string",
5705+
"minLength": 1
5706+
},
5707+
{
5708+
"type": "null"
5709+
}
5710+
],
5711+
"description": "The assignee in format 'user:ID' or 'team:ID_OR_SLUG' where ID is numeric. Pass null to unassign the issue. Example: 'user:123456', 'team:789', or 'team:my-team-slug'. Use `execute_sentry_tool(name='whoami', arguments={})` to find your user ID."
57045712
},
57055713
"ignoreMode": {
57065714
"type": "string",

‎packages/mcp-core/src/tools/catalog/update-issue.test.ts‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,44 @@ describe("update_issue", () => {
9696
`);
9797
});
9898

99+
it("unassigns an issue", async () => {
100+
let requestBody: Record<string, unknown> | undefined;
101+
const currentIssue = createIssue();
102+
const updatedIssue = createIssue({ assignedTo: null });
103+
104+
mswServer.use(
105+
http.get(
106+
"https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-41/",
107+
() => HttpResponse.json(currentIssue),
108+
),
109+
http.put(
110+
"https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-41/",
111+
async ({ request }) => {
112+
requestBody = (await request.json()) as Record<string, unknown>;
113+
return HttpResponse.json(updatedIssue);
114+
},
115+
),
116+
);
117+
118+
const result = await updateIssue.handler(
119+
{
120+
organizationSlug: "sentry-mcp-evals",
121+
issueId: "CLOUDFLARE-MCP-41",
122+
status: undefined,
123+
assignedTo: null,
124+
issueUrl: undefined,
125+
regionUrl: null,
126+
},
127+
serverContext,
128+
);
129+
130+
expect(requestBody).toEqual({ assignedTo: "" });
131+
expect(result).toContain(
132+
"**Assigned To**: Jane Developer → **Unassigned**",
133+
);
134+
expect(result).toContain("**Assigned To**: Unassigned");
135+
});
136+
99137
it("skips status updates when the requested status is already set", async () => {
100138
let putCalled = false;
101139
const currentIssue = createIssue({

‎packages/mcp-core/src/tools/catalog/update-issue.ts‎

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -339,10 +339,18 @@ function buildNoChangesOutput(params: {
339339

340340
function isAssigneeAlreadySet(
341341
issue: Issue,
342-
requestedAssignee: string | undefined,
342+
requestedAssignee: string | null | undefined,
343343
currentUserId: string | null | undefined,
344344
): boolean {
345-
if (!requestedAssignee || !issue.assignedTo) {
345+
if (requestedAssignee === undefined) {
346+
return false;
347+
}
348+
349+
if (requestedAssignee === null) {
350+
return !issue.assignedTo;
351+
}
352+
353+
if (!issue.assignedTo) {
346354
return false;
347355
}
348356

@@ -622,12 +630,13 @@ export default defineTool({
622630
description: [
623631
"Update a Sentry issue's status or assignment.",
624632
"",
625-
"Use this to resolve, reopen, assign, or ignore an issue.",
633+
"Use this to resolve, reopen, assign, unassign, or ignore an issue.",
626634
"",
627635
"<examples>",
628636
"```",
629637
"update_issue(organizationSlug='my-org', issueId='PROJECT-123', status='resolved')",
630638
"update_issue(organizationSlug='my-org', issueId='PROJECT-123', assignedTo='user:123456')",
639+
"update_issue(organizationSlug='my-org', issueId='PROJECT-123', assignedTo=null)",
631640
"update_issue(organizationSlug='my-org', issueId='PROJECT-123', status='ignored')",
632641
"update_issue(organizationSlug='my-org', issueId='PROJECT-123', status='ignored', ignoreMode='forever')",
633642
"update_issue(organizationSlug='my-org', issueId='PROJECT-123', status='ignored', ignoreMode='untilOccurrenceCount', ignoreCount=100, ignoreWindowMinutes=60)",
@@ -638,7 +647,7 @@ export default defineTool({
638647
"<hints>",
639648
"- Provide `issueUrl` or `organizationSlug` + `issueId`.",
640649
"- At least one of `status` or `assignedTo` is required.",
641-
"- `assignedTo` format: `user:ID` or `team:ID_OR_SLUG`.",
650+
"- Omit `assignedTo` to leave assignment unchanged. Pass `null` to unassign; otherwise use `user:ID` or `team:ID_OR_SLUG`.",
642651
"- Use `execute_sentry_tool(name='whoami', arguments={})` to find your user ID for self-assignment.",
643652
"- Status values: `resolved`, `resolvedInNextRelease`, `unresolved`, `ignored`.",
644653
"- `status='ignored'` defaults to `ignoreMode='untilEscalating'`.",
@@ -688,7 +697,7 @@ export default defineTool({
688697
}
689698

690699
// Validate that at least one update parameter is provided
691-
if (!params.status && !params.assignedTo) {
700+
if (params.status === undefined && params.assignedTo === undefined) {
692701
throw new UserInputError(
693702
"At least one of `status` or `assignedTo` must be provided to update the issue",
694703
);
@@ -762,7 +771,11 @@ export default defineTool({
762771
currentIssue.shortId,
763772
);
764773

765-
if (!updateStatus && !updateAssignedTo && !updateIgnore) {
774+
if (
775+
updateStatus === undefined &&
776+
updateAssignedTo === undefined &&
777+
updateIgnore === undefined
778+
) {
766779
const commentResult = await tryPostReasonComment(
767780
apiService,
768781
orgSlug,
@@ -834,7 +847,7 @@ export default defineTool({
834847
}
835848
}
836849

837-
if (updateAssignedTo && assignmentChanged) {
850+
if (updateAssignedTo !== undefined && assignmentChanged) {
838851
const oldAssignee = formatAssignedTo(currentIssue.assignedTo ?? null);
839852
const newAssignee =
840853
params.assignedTo === "me"

0 commit comments

Comments
 (0)