Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -751,6 +751,7 @@ The following sets of tools are available:

- **assign_copilot_to_issue** - Assign Copilot to issue
- **Required OAuth Scopes**: `repo`
- `base_ref`: Git reference (e.g., branch) that the agent will start its work from. If not specified, defaults to the repository's default branch (string, optional)
- `issue_number`: Issue number (number, required)
- `owner`: Repository owner (string, required)
- `repo`: Repository name (string, required)
Expand Down
6 changes: 6 additions & 0 deletions internal/ghmcp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,12 @@ type bearerAuthTransport struct {
func (t *bearerAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) {
req = req.Clone(req.Context())
req.Header.Set("Authorization", "Bearer "+t.token)

// Check for GraphQL-Features in context and add header if present
if features := github.GetGraphQLFeatures(req.Context()); len(features) > 0 {
req.Header.Set("GraphQL-Features", strings.Join(features, ", "))
}

return t.transport.RoundTrip(req)
}

Expand Down
4 changes: 4 additions & 0 deletions pkg/github/__toolsnaps__/assign_copilot_to_issue.snap
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
"inputSchema": {
"type": "object",
"properties": {
"base_ref": {
"type": "string",
"description": "Git reference (e.g., branch) that the agent will start its work from. If not specified, defaults to the repository's default branch"
},
"issue_number": {
"type": "number",
"description": "Issue number"
Expand Down
92 changes: 76 additions & 16 deletions pkg/github/issues.go
Original file line number Diff line number Diff line change
Expand Up @@ -1646,6 +1646,10 @@ func AssignCopilotToIssue(t translations.TranslationHelperFunc) inventory.Server
Type: "number",
Description: "Issue number",
},
"base_ref": {
Type: "string",
Description: "Git reference (e.g., branch) that the agent will start its work from. If not specified, defaults to the repository's default branch",
},
},
Required: []string{"owner", "repo", "issue_number"},
},
Expand All @@ -1656,6 +1660,7 @@ func AssignCopilotToIssue(t translations.TranslationHelperFunc) inventory.Server
Owner string `mapstructure:"owner"`
Repo string `mapstructure:"repo"`
IssueNumber int32 `mapstructure:"issue_number"`
BaseRef string `mapstructure:"base_ref"`
}
if err := mapstructure.Decode(args, &params); err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
Expand Down Expand Up @@ -1724,10 +1729,10 @@ func AssignCopilotToIssue(t translations.TranslationHelperFunc) inventory.Server
return utils.NewToolResultError("copilot isn't available as an assignee for this issue. Please inform the user to visit https://docs.github.com/en/copilot/using-github-copilot/using-copilot-coding-agent-to-work-on-tasks/about-assigning-tasks-to-copilot for more information."), nil, nil
}

// Next let's get the GQL Node ID and current assignees for this issue because the only way to
// assign copilot is to use replaceActorsForAssignable which requires the full list.
// Next, get the issue ID and repository ID
var getIssueQuery struct {
Repository struct {
ID githubv4.ID
Issue struct {
ID githubv4.ID
Assignees struct {
Expand All @@ -1749,30 +1754,54 @@ func AssignCopilotToIssue(t translations.TranslationHelperFunc) inventory.Server
return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "failed to get issue ID", err), nil, nil
}

// Finally, do the assignment. Just for reference, assigning copilot to an issue that it is already
// assigned to seems to have no impact (which is a good thing).
var assignCopilotMutation struct {
ReplaceActorsForAssignable struct {
Typename string `graphql:"__typename"` // Not required but we need a selector or GQL errors
} `graphql:"replaceActorsForAssignable(input: $input)"`
}

// Build the assignee IDs list including copilot
actorIDs := make([]githubv4.ID, len(getIssueQuery.Repository.Issue.Assignees.Nodes)+1)
for i, node := range getIssueQuery.Repository.Issue.Assignees.Nodes {
actorIDs[i] = node.ID
}
actorIDs[len(getIssueQuery.Repository.Issue.Assignees.Nodes)] = copilotAssignee.ID

// Prepare agent assignment input
emptyString := githubv4.String("")
agentAssignment := &AgentAssignmentInput{
CustomAgent: &emptyString,
CustomInstructions: &emptyString,
TargetRepositoryID: getIssueQuery.Repository.ID,
}

// Add base ref if provided
if params.BaseRef != "" {
baseRef := githubv4.String(params.BaseRef)
agentAssignment.BaseRef = &baseRef
}

// Execute the updateIssue mutation with the GraphQL-Features header
// This header is required for the agent assignment API which is not GA yet
var updateIssueMutation struct {
UpdateIssue struct {
Issue struct {
ID githubv4.ID
Number githubv4.Int
URL githubv4.String
}
} `graphql:"updateIssue(input: $input)"`
}

// Add the GraphQL-Features header for the agent assignment API
// The header will be read by the HTTP transport if it's configured to do so
ctxWithFeatures := withGraphQLFeatures(ctx, "issues_copilot_assignment_api_support")

if err := client.Mutate(
ctx,
&assignCopilotMutation,
ReplaceActorsForAssignableInput{
AssignableID: getIssueQuery.Repository.Issue.ID,
ActorIDs: actorIDs,
ctxWithFeatures,
&updateIssueMutation,
UpdateIssueInput{
ID: getIssueQuery.Repository.Issue.ID,
AssigneeIDs: actorIDs,
AgentAssignment: agentAssignment,
},
nil,
); err != nil {
return nil, nil, fmt.Errorf("failed to replace actors for assignable: %w", err)
return nil, nil, fmt.Errorf("failed to update issue with agent assignment: %w", err)
}

return utils.NewToolResultText("successfully assigned copilot to issue"), nil, nil
Expand All @@ -1784,6 +1813,21 @@ type ReplaceActorsForAssignableInput struct {
ActorIDs []githubv4.ID `json:"actorIds"`
}

// AgentAssignmentInput represents the input for assigning an agent to an issue.
type AgentAssignmentInput struct {
BaseRef *githubv4.String `json:"baseRef,omitempty"`
CustomAgent *githubv4.String `json:"customAgent,omitempty"`
CustomInstructions *githubv4.String `json:"customInstructions,omitempty"`
TargetRepositoryID githubv4.ID `json:"targetRepositoryId"`
}

// UpdateIssueInput represents the input for updating an issue with agent assignment.
type UpdateIssueInput struct {
ID githubv4.ID `json:"id"`
AssigneeIDs []githubv4.ID `json:"assigneeIds"`
AgentAssignment *AgentAssignmentInput `json:"agentAssignment,omitempty"`
}

// parseISOTimestamp parses an ISO 8601 timestamp string into a time.Time object.
// Returns the parsed time or an error if parsing fails.
// Example formats supported: "2023-01-15T14:30:00Z", "2023-01-15"
Expand Down Expand Up @@ -1869,3 +1913,19 @@ func AssignCodingAgentPrompt(t translations.TranslationHelperFunc) inventory.Ser
},
)
}

// graphQLFeaturesKey is a context key for GraphQL feature flags
type graphQLFeaturesKey struct{}

// withGraphQLFeatures adds GraphQL feature flags to the context
func withGraphQLFeatures(ctx context.Context, features ...string) context.Context {
return context.WithValue(ctx, graphQLFeaturesKey{}, features)
}

// GetGraphQLFeatures retrieves GraphQL feature flags from the context
func GetGraphQLFeatures(ctx context.Context) []string {
if features, ok := ctx.Value(graphQLFeaturesKey{}).([]string); ok {
return features
}
return nil
}
Loading
Loading