@@ -309,27 +309,131 @@ func GranularUpdateIssueMilestone(t translations.TranslationHelperFunc) inventor
309309 )
310310}
311311
312+ // issueTypeWithRationale represents the object form of the issue type field,
313+ // allowing a rationale to be sent alongside the type name.
314+ type issueTypeWithRationale struct {
315+ Value string `json:"value"`
316+ Rationale string `json:"rationale"`
317+ }
318+
319+ // issueTypeUpdateRequest is a custom request body for updating an issue type
320+ // with an optional rationale, using the object form that the REST API accepts.
321+ type issueTypeUpdateRequest struct {
322+ Type issueTypeWithRationale `json:"type"`
323+ }
324+
312325// GranularUpdateIssueType creates a tool to update an issue's type.
313326func GranularUpdateIssueType (t translations.TranslationHelperFunc ) inventory.ServerTool {
314- return issueUpdateTool (t ,
315- "update_issue_type" ,
316- "Update the type of an existing issue (e.g. 'bug', 'feature')." ,
317- "Update Issue Type" ,
318- map [string ]* jsonschema.Schema {
319- "issue_type" : {
320- Type : "string" ,
321- Description : "The issue type to set" ,
327+ st := NewTool (
328+ ToolsetMetadataIssues ,
329+ mcp.Tool {
330+ Name : "update_issue_type" ,
331+ Description : t ("TOOL_UPDATE_ISSUE_TYPE_DESCRIPTION" , "Update the type of an existing issue (e.g. 'bug', 'feature')." ),
332+ Annotations : & mcp.ToolAnnotations {
333+ Title : t ("TOOL_UPDATE_ISSUE_TYPE_USER_TITLE" , "Update Issue Type" ),
334+ ReadOnlyHint : false ,
335+ DestructiveHint : jsonschema .Ptr (false ),
336+ OpenWorldHint : jsonschema .Ptr (true ),
337+ },
338+ InputSchema : & jsonschema.Schema {
339+ Type : "object" ,
340+ Properties : map [string ]* jsonschema.Schema {
341+ "owner" : {
342+ Type : "string" ,
343+ Description : "Repository owner (username or organization)" ,
344+ },
345+ "repo" : {
346+ Type : "string" ,
347+ Description : "Repository name" ,
348+ },
349+ "issue_number" : {
350+ Type : "number" ,
351+ Description : "The issue number to update" ,
352+ Minimum : jsonschema .Ptr (1.0 ),
353+ },
354+ "issue_type" : {
355+ Type : "string" ,
356+ Description : "The issue type to set" ,
357+ },
358+ "rationale" : {
359+ Type : "string" ,
360+ Description : "One concise sentence explaining what specifically about the issue led you to choose this type. " +
361+ "State the concrete signal (e.g. 'Reports a crash when saving' → bug, 'Asks for dark mode support' → feature)." ,
362+ MaxLength : jsonschema .Ptr (280 ),
363+ },
364+ },
365+ Required : []string {"owner" , "repo" , "issue_number" , "issue_type" },
322366 },
323367 },
324- []string {"issue_type" },
325- func (args map [string ]any ) (* github.IssueRequest , error ) {
368+ []scopes.Scope {scopes .Repo },
369+ func (ctx context.Context , deps ToolDependencies , _ * mcp.CallToolRequest , args map [string ]any ) (* mcp.CallToolResult , any , error ) {
370+ owner , err := RequiredParam [string ](args , "owner" )
371+ if err != nil {
372+ return utils .NewToolResultError (err .Error ()), nil , nil
373+ }
374+ repo , err := RequiredParam [string ](args , "repo" )
375+ if err != nil {
376+ return utils .NewToolResultError (err .Error ()), nil , nil
377+ }
378+ issueNumber , err := RequiredInt (args , "issue_number" )
379+ if err != nil {
380+ return utils .NewToolResultError (err .Error ()), nil , nil
381+ }
326382 issueType , err := RequiredParam [string ](args , "issue_type" )
327383 if err != nil {
328- return nil , err
384+ return utils .NewToolResultError (err .Error ()), nil , nil
385+ }
386+ rationale , err := OptionalParam [string ](args , "rationale" )
387+ if err != nil {
388+ return utils .NewToolResultError (err .Error ()), nil , nil
389+ }
390+ rationale = strings .TrimSpace (rationale )
391+ if len ([]rune (rationale )) > 280 {
392+ return utils .NewToolResultError ("parameter rationale must be 280 characters or less" ), nil , nil
393+ }
394+
395+ client , err := deps .GetClient (ctx )
396+ if err != nil {
397+ return utils .NewToolResultErrorFromErr ("failed to get GitHub client" , err ), nil , nil
398+ }
399+
400+ var body any
401+ if rationale != "" {
402+ body = & issueTypeUpdateRequest {
403+ Type : issueTypeWithRationale {
404+ Value : issueType ,
405+ Rationale : rationale ,
406+ },
407+ }
408+ } else {
409+ body = & github.IssueRequest {Type : & issueType }
410+ }
411+
412+ apiURL := fmt .Sprintf ("repos/%s/%s/issues/%d" , owner , repo , issueNumber )
413+ req , err := client .NewRequest ("PATCH" , apiURL , body )
414+ if err != nil {
415+ return utils .NewToolResultErrorFromErr ("failed to create request" , err ), nil , nil
329416 }
330- return & github.IssueRequest {Type : & issueType }, nil
417+
418+ issue := & github.Issue {}
419+ resp , err := client .Do (ctx , req , issue )
420+ if err != nil {
421+ return ghErrors .NewGitHubAPIErrorResponse (ctx , "failed to update issue" , resp , err ), nil , nil
422+ }
423+ defer func () { _ = resp .Body .Close () }()
424+
425+ r , err := json .Marshal (MinimalResponse {
426+ ID : fmt .Sprintf ("%d" , issue .GetID ()),
427+ URL : issue .GetHTMLURL (),
428+ })
429+ if err != nil {
430+ return utils .NewToolResultErrorFromErr ("failed to marshal response" , err ), nil , nil
431+ }
432+ return utils .NewToolResultText (string (r )), nil , nil
331433 },
332434 )
435+ st .FeatureFlagEnable = FeatureFlagIssuesGranular
436+ return st
333437}
334438
335439// GranularUpdateIssueState creates a tool to update an issue's state.
0 commit comments