diff --git a/pkg/github/__toolsnaps__/add_issue_comment.snap b/pkg/github/__toolsnaps__/add_issue_comment.snap index 92eeb1ce8..669f54a3d 100644 --- a/pkg/github/__toolsnaps__/add_issue_comment.snap +++ b/pkg/github/__toolsnaps__/add_issue_comment.snap @@ -11,7 +11,7 @@ "type": "string" }, "issue_number": { - "description": "Issue number to comment on", + "description": "Issue number", "type": "number" }, "owner": { diff --git a/pkg/github/__toolsnaps__/create_or_update_file.snap b/pkg/github/__toolsnaps__/create_or_update_file.snap index dfbb34423..7932fd95f 100644 --- a/pkg/github/__toolsnaps__/create_or_update_file.snap +++ b/pkg/github/__toolsnaps__/create_or_update_file.snap @@ -19,7 +19,7 @@ "type": "string" }, "owner": { - "description": "Repository owner (username or organization)", + "description": "Repository owner", "type": "string" }, "path": { diff --git a/pkg/github/__toolsnaps__/delete_file.snap b/pkg/github/__toolsnaps__/delete_file.snap index 2588ea5c5..2468fa55d 100644 --- a/pkg/github/__toolsnaps__/delete_file.snap +++ b/pkg/github/__toolsnaps__/delete_file.snap @@ -16,7 +16,7 @@ "type": "string" }, "owner": { - "description": "Repository owner (username or organization)", + "description": "Repository owner", "type": "string" }, "path": { diff --git a/pkg/github/__toolsnaps__/get_code_scanning_alert.snap b/pkg/github/__toolsnaps__/get_code_scanning_alert.snap index eedc20b46..6010c9282 100644 --- a/pkg/github/__toolsnaps__/get_code_scanning_alert.snap +++ b/pkg/github/__toolsnaps__/get_code_scanning_alert.snap @@ -7,15 +7,15 @@ "inputSchema": { "properties": { "alertNumber": { - "description": "The number of the alert.", + "description": "Alert number", "type": "number" }, "owner": { - "description": "The owner of the repository.", + "description": "Repository owner", "type": "string" }, "repo": { - "description": "The name of the repository.", + "description": "Repository name", "type": "string" } }, diff --git a/pkg/github/__toolsnaps__/get_file_contents.snap b/pkg/github/__toolsnaps__/get_file_contents.snap index b3975abbc..93a8ede7b 100644 --- a/pkg/github/__toolsnaps__/get_file_contents.snap +++ b/pkg/github/__toolsnaps__/get_file_contents.snap @@ -7,7 +7,7 @@ "inputSchema": { "properties": { "owner": { - "description": "Repository owner (username or organization)", + "description": "Repository owner", "type": "string" }, "path": { diff --git a/pkg/github/__toolsnaps__/get_issue.snap b/pkg/github/__toolsnaps__/get_issue.snap index eab2b8722..8613e8b4f 100644 --- a/pkg/github/__toolsnaps__/get_issue.snap +++ b/pkg/github/__toolsnaps__/get_issue.snap @@ -7,15 +7,15 @@ "inputSchema": { "properties": { "issue_number": { - "description": "The number of the issue", + "description": "Issue number", "type": "number" }, "owner": { - "description": "The owner of the repository", + "description": "Repository owner", "type": "string" }, "repo": { - "description": "The name of the repository", + "description": "Repository name", "type": "string" } }, diff --git a/pkg/github/__toolsnaps__/list_code_scanning_alerts.snap b/pkg/github/__toolsnaps__/list_code_scanning_alerts.snap index 470f0d01f..c52c9d181 100644 --- a/pkg/github/__toolsnaps__/list_code_scanning_alerts.snap +++ b/pkg/github/__toolsnaps__/list_code_scanning_alerts.snap @@ -7,7 +7,7 @@ "inputSchema": { "properties": { "owner": { - "description": "The owner of the repository.", + "description": "Repository owner", "type": "string" }, "ref": { @@ -15,7 +15,7 @@ "type": "string" }, "repo": { - "description": "The name of the repository.", + "description": "Repository name", "type": "string" }, "severity": { diff --git a/pkg/github/__toolsnaps__/update_issue.snap b/pkg/github/__toolsnaps__/update_issue.snap index 4bcae7ba7..7e77eb121 100644 --- a/pkg/github/__toolsnaps__/update_issue.snap +++ b/pkg/github/__toolsnaps__/update_issue.snap @@ -18,7 +18,7 @@ "type": "string" }, "issue_number": { - "description": "Issue number to update", + "description": "Issue number", "type": "number" }, "labels": { diff --git a/pkg/github/__toolsnaps__/update_pull_request.snap b/pkg/github/__toolsnaps__/update_pull_request.snap index 765983afd..05fd66546 100644 --- a/pkg/github/__toolsnaps__/update_pull_request.snap +++ b/pkg/github/__toolsnaps__/update_pull_request.snap @@ -23,7 +23,7 @@ "type": "string" }, "pullNumber": { - "description": "Pull request number to update", + "description": "Pull request number", "type": "number" }, "repo": { diff --git a/pkg/github/actions.go b/pkg/github/actions.go index 8c7b08a85..a806e5302 100644 --- a/pkg/github/actions.go +++ b/pkg/github/actions.go @@ -29,14 +29,8 @@ func ListWorkflows(getClient GetClientFn, t translations.TranslationHelperFunc) Title: t("TOOL_LIST_WORKFLOWS_USER_TITLE", "List workflows"), ReadOnlyHint: ToBoolPtr(true), }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description(DescriptionRepositoryOwner), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description(DescriptionRepositoryName), - ), + WithOwnerParam(), + WithRepoParam(), mcp.WithNumber("per_page", mcp.Description("The number of results per page (max 100)"), ), @@ -66,7 +60,7 @@ func ListWorkflows(getClient GetClientFn, t translations.TranslationHelperFunc) client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(ErrFailedToGetGitHubClient, err) } // Set up list options @@ -81,12 +75,7 @@ func ListWorkflows(getClient GetClientFn, t translations.TranslationHelperFunc) } defer func() { _ = resp.Body.Close() }() - r, err := json.Marshal(workflows) - if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) - } - - return mcp.NewToolResultText(string(r)), nil + return MarshalledTextResult(workflows), nil } } @@ -98,14 +87,8 @@ func ListWorkflowRuns(getClient GetClientFn, t translations.TranslationHelperFun Title: t("TOOL_LIST_WORKFLOW_RUNS_USER_TITLE", "List workflow runs"), ReadOnlyHint: ToBoolPtr(true), }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description(DescriptionRepositoryOwner), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description(DescriptionRepositoryName), - ), + WithOwnerParam(), + WithRepoParam(), mcp.WithString("workflow_id", mcp.Required(), mcp.Description("The workflow ID or workflow file name"), @@ -208,7 +191,7 @@ func ListWorkflowRuns(getClient GetClientFn, t translations.TranslationHelperFun client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(ErrFailedToGetGitHubClient, err) } // Set up list options @@ -229,12 +212,7 @@ func ListWorkflowRuns(getClient GetClientFn, t translations.TranslationHelperFun } defer func() { _ = resp.Body.Close() }() - r, err := json.Marshal(workflowRuns) - if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) - } - - return mcp.NewToolResultText(string(r)), nil + return MarshalledTextResult(workflowRuns), nil } } @@ -246,14 +224,8 @@ func RunWorkflow(getClient GetClientFn, t translations.TranslationHelperFunc) (t Title: t("TOOL_RUN_WORKFLOW_USER_TITLE", "Run workflow"), ReadOnlyHint: ToBoolPtr(false), }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description(DescriptionRepositoryOwner), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description(DescriptionRepositoryName), - ), + WithOwnerParam(), + WithRepoParam(), mcp.WithString("workflow_id", mcp.Required(), mcp.Description("The workflow ID (numeric) or workflow file name (e.g., main.yml, ci.yaml)"), @@ -294,7 +266,7 @@ func RunWorkflow(getClient GetClientFn, t translations.TranslationHelperFunc) (t client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(ErrFailedToGetGitHubClient, err) } event := github.CreateWorkflowDispatchEventRequest{ @@ -328,12 +300,7 @@ func RunWorkflow(getClient GetClientFn, t translations.TranslationHelperFunc) (t "status_code": resp.StatusCode, } - r, err := json.Marshal(result) - if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) - } - - return mcp.NewToolResultText(string(r)), nil + return MarshalledTextResult(result), nil } } @@ -345,14 +312,8 @@ func GetWorkflowRun(getClient GetClientFn, t translations.TranslationHelperFunc) Title: t("TOOL_GET_WORKFLOW_RUN_USER_TITLE", "Get workflow run"), ReadOnlyHint: ToBoolPtr(true), }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description(DescriptionRepositoryOwner), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description(DescriptionRepositoryName), - ), + WithOwnerParam(), + WithRepoParam(), mcp.WithNumber("run_id", mcp.Required(), mcp.Description("The unique identifier of the workflow run"), @@ -375,7 +336,7 @@ func GetWorkflowRun(getClient GetClientFn, t translations.TranslationHelperFunc) client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(ErrFailedToGetGitHubClient, err) } workflowRun, resp, err := client.Actions.GetWorkflowRunByID(ctx, owner, repo, runID) @@ -384,12 +345,7 @@ func GetWorkflowRun(getClient GetClientFn, t translations.TranslationHelperFunc) } defer func() { _ = resp.Body.Close() }() - r, err := json.Marshal(workflowRun) - if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) - } - - return mcp.NewToolResultText(string(r)), nil + return MarshalledTextResult(workflowRun), nil } } @@ -401,14 +357,8 @@ func GetWorkflowRunLogs(getClient GetClientFn, t translations.TranslationHelperF Title: t("TOOL_GET_WORKFLOW_RUN_LOGS_USER_TITLE", "Get workflow run logs"), ReadOnlyHint: ToBoolPtr(true), }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description(DescriptionRepositoryOwner), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description(DescriptionRepositoryName), - ), + WithOwnerParam(), + WithRepoParam(), mcp.WithNumber("run_id", mcp.Required(), mcp.Description("The unique identifier of the workflow run"), @@ -431,7 +381,7 @@ func GetWorkflowRunLogs(getClient GetClientFn, t translations.TranslationHelperF client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(ErrFailedToGetGitHubClient, err) } // Get the download URL for the logs @@ -450,12 +400,7 @@ func GetWorkflowRunLogs(getClient GetClientFn, t translations.TranslationHelperF "optimization_tip": "Use: get_job_logs with parameters {run_id: " + fmt.Sprintf("%d", runID) + ", failed_only: true} for more efficient failed job debugging", } - r, err := json.Marshal(result) - if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) - } - - return mcp.NewToolResultText(string(r)), nil + return MarshalledTextResult(result), nil } } @@ -467,14 +412,8 @@ func ListWorkflowJobs(getClient GetClientFn, t translations.TranslationHelperFun Title: t("TOOL_LIST_WORKFLOW_JOBS_USER_TITLE", "List workflow jobs"), ReadOnlyHint: ToBoolPtr(true), }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description(DescriptionRepositoryOwner), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description(DescriptionRepositoryName), - ), + WithOwnerParam(), + WithRepoParam(), mcp.WithNumber("run_id", mcp.Required(), mcp.Description("The unique identifier of the workflow run"), @@ -523,7 +462,7 @@ func ListWorkflowJobs(getClient GetClientFn, t translations.TranslationHelperFun client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(ErrFailedToGetGitHubClient, err) } // Set up list options @@ -547,12 +486,7 @@ func ListWorkflowJobs(getClient GetClientFn, t translations.TranslationHelperFun "optimization_tip": "For debugging failed jobs, consider using get_job_logs with failed_only=true and run_id=" + fmt.Sprintf("%d", runID) + " to get logs directly without needing to list jobs first", } - r, err := json.Marshal(response) - if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) - } - - return mcp.NewToolResultText(string(r)), nil + return MarshalledTextResult(response), nil } } @@ -564,14 +498,8 @@ func GetJobLogs(getClient GetClientFn, t translations.TranslationHelperFunc) (to Title: t("TOOL_GET_JOB_LOGS_USER_TITLE", "Get job logs"), ReadOnlyHint: ToBoolPtr(true), }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description(DescriptionRepositoryOwner), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description(DescriptionRepositoryName), - ), + WithOwnerParam(), + WithRepoParam(), mcp.WithNumber("job_id", mcp.Description("The unique identifier of the workflow job (required for single job logs)"), ), @@ -627,7 +555,7 @@ func GetJobLogs(getClient GetClientFn, t translations.TranslationHelperFunc) (to client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(ErrFailedToGetGitHubClient, err) } // Validate parameters @@ -707,12 +635,7 @@ func handleFailedJobLogs(ctx context.Context, client *github.Client, owner, repo "return_format": map[string]bool{"content": returnContent, "urls": !returnContent}, } - r, err := json.Marshal(result) - if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) - } - - return mcp.NewToolResultText(string(r)), nil + return MarshalledTextResult(result), nil } // handleSingleJobLogs gets logs for a single job @@ -722,12 +645,7 @@ func handleSingleJobLogs(ctx context.Context, client *github.Client, owner, repo return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to get job logs", resp, err), nil } - r, err := json.Marshal(jobResult) - if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) - } - - return mcp.NewToolResultText(string(r)), nil + return MarshalledTextResult(jobResult), nil } // getJobLogData retrieves log data for a single job, either as URL or content @@ -821,14 +739,8 @@ func RerunWorkflowRun(getClient GetClientFn, t translations.TranslationHelperFun Title: t("TOOL_RERUN_WORKFLOW_RUN_USER_TITLE", "Rerun workflow run"), ReadOnlyHint: ToBoolPtr(false), }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description(DescriptionRepositoryOwner), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description(DescriptionRepositoryName), - ), + WithOwnerParam(), + WithRepoParam(), mcp.WithNumber("run_id", mcp.Required(), mcp.Description("The unique identifier of the workflow run"), @@ -851,7 +763,7 @@ func RerunWorkflowRun(getClient GetClientFn, t translations.TranslationHelperFun client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(ErrFailedToGetGitHubClient, err) } resp, err := client.Actions.RerunWorkflowByID(ctx, owner, repo, runID) @@ -867,12 +779,7 @@ func RerunWorkflowRun(getClient GetClientFn, t translations.TranslationHelperFun "status_code": resp.StatusCode, } - r, err := json.Marshal(result) - if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) - } - - return mcp.NewToolResultText(string(r)), nil + return MarshalledTextResult(result), nil } } @@ -884,14 +791,8 @@ func RerunFailedJobs(getClient GetClientFn, t translations.TranslationHelperFunc Title: t("TOOL_RERUN_FAILED_JOBS_USER_TITLE", "Rerun failed jobs"), ReadOnlyHint: ToBoolPtr(false), }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description(DescriptionRepositoryOwner), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description(DescriptionRepositoryName), - ), + WithOwnerParam(), + WithRepoParam(), mcp.WithNumber("run_id", mcp.Required(), mcp.Description("The unique identifier of the workflow run"), @@ -914,7 +815,7 @@ func RerunFailedJobs(getClient GetClientFn, t translations.TranslationHelperFunc client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(ErrFailedToGetGitHubClient, err) } resp, err := client.Actions.RerunFailedJobsByID(ctx, owner, repo, runID) @@ -930,12 +831,7 @@ func RerunFailedJobs(getClient GetClientFn, t translations.TranslationHelperFunc "status_code": resp.StatusCode, } - r, err := json.Marshal(result) - if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) - } - - return mcp.NewToolResultText(string(r)), nil + return MarshalledTextResult(result), nil } } @@ -947,14 +843,8 @@ func CancelWorkflowRun(getClient GetClientFn, t translations.TranslationHelperFu Title: t("TOOL_CANCEL_WORKFLOW_RUN_USER_TITLE", "Cancel workflow run"), ReadOnlyHint: ToBoolPtr(false), }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description(DescriptionRepositoryOwner), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description(DescriptionRepositoryName), - ), + WithOwnerParam(), + WithRepoParam(), mcp.WithNumber("run_id", mcp.Required(), mcp.Description("The unique identifier of the workflow run"), @@ -977,7 +867,7 @@ func CancelWorkflowRun(getClient GetClientFn, t translations.TranslationHelperFu client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(ErrFailedToGetGitHubClient, err) } resp, err := client.Actions.CancelWorkflowRunByID(ctx, owner, repo, runID) @@ -993,12 +883,7 @@ func CancelWorkflowRun(getClient GetClientFn, t translations.TranslationHelperFu "status_code": resp.StatusCode, } - r, err := json.Marshal(result) - if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) - } - - return mcp.NewToolResultText(string(r)), nil + return MarshalledTextResult(result), nil } } @@ -1010,14 +895,8 @@ func ListWorkflowRunArtifacts(getClient GetClientFn, t translations.TranslationH Title: t("TOOL_LIST_WORKFLOW_RUN_ARTIFACTS_USER_TITLE", "List workflow artifacts"), ReadOnlyHint: ToBoolPtr(true), }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description(DescriptionRepositoryOwner), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description(DescriptionRepositoryName), - ), + WithOwnerParam(), + WithRepoParam(), mcp.WithNumber("run_id", mcp.Required(), mcp.Description("The unique identifier of the workflow run"), @@ -1056,7 +935,7 @@ func ListWorkflowRunArtifacts(getClient GetClientFn, t translations.TranslationH client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(ErrFailedToGetGitHubClient, err) } // Set up list options @@ -1071,12 +950,7 @@ func ListWorkflowRunArtifacts(getClient GetClientFn, t translations.TranslationH } defer func() { _ = resp.Body.Close() }() - r, err := json.Marshal(artifacts) - if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) - } - - return mcp.NewToolResultText(string(r)), nil + return MarshalledTextResult(artifacts), nil } } @@ -1088,14 +962,8 @@ func DownloadWorkflowRunArtifact(getClient GetClientFn, t translations.Translati Title: t("TOOL_DOWNLOAD_WORKFLOW_RUN_ARTIFACT_USER_TITLE", "Download workflow artifact"), ReadOnlyHint: ToBoolPtr(true), }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description(DescriptionRepositoryOwner), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description(DescriptionRepositoryName), - ), + WithOwnerParam(), + WithRepoParam(), mcp.WithNumber("artifact_id", mcp.Required(), mcp.Description("The unique identifier of the artifact"), @@ -1118,7 +986,7 @@ func DownloadWorkflowRunArtifact(getClient GetClientFn, t translations.Translati client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(ErrFailedToGetGitHubClient, err) } // Get the download URL for the artifact @@ -1136,12 +1004,7 @@ func DownloadWorkflowRunArtifact(getClient GetClientFn, t translations.Translati "artifact_id": artifactID, } - r, err := json.Marshal(result) - if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) - } - - return mcp.NewToolResultText(string(r)), nil + return MarshalledTextResult(result), nil } } @@ -1154,14 +1017,8 @@ func DeleteWorkflowRunLogs(getClient GetClientFn, t translations.TranslationHelp ReadOnlyHint: ToBoolPtr(false), DestructiveHint: ToBoolPtr(true), }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description(DescriptionRepositoryOwner), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description(DescriptionRepositoryName), - ), + WithOwnerParam(), + WithRepoParam(), mcp.WithNumber("run_id", mcp.Required(), mcp.Description("The unique identifier of the workflow run"), @@ -1184,7 +1041,7 @@ func DeleteWorkflowRunLogs(getClient GetClientFn, t translations.TranslationHelp client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(ErrFailedToGetGitHubClient, err) } resp, err := client.Actions.DeleteWorkflowRunLogs(ctx, owner, repo, runID) @@ -1200,12 +1057,7 @@ func DeleteWorkflowRunLogs(getClient GetClientFn, t translations.TranslationHelp "status_code": resp.StatusCode, } - r, err := json.Marshal(result) - if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) - } - - return mcp.NewToolResultText(string(r)), nil + return MarshalledTextResult(result), nil } } @@ -1217,14 +1069,8 @@ func GetWorkflowRunUsage(getClient GetClientFn, t translations.TranslationHelper Title: t("TOOL_GET_WORKFLOW_RUN_USAGE_USER_TITLE", "Get workflow usage"), ReadOnlyHint: ToBoolPtr(true), }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description(DescriptionRepositoryOwner), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description(DescriptionRepositoryName), - ), + WithOwnerParam(), + WithRepoParam(), mcp.WithNumber("run_id", mcp.Required(), mcp.Description("The unique identifier of the workflow run"), @@ -1247,7 +1093,7 @@ func GetWorkflowRunUsage(getClient GetClientFn, t translations.TranslationHelper client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(ErrFailedToGetGitHubClient, err) } usage, resp, err := client.Actions.GetWorkflowRunUsageByID(ctx, owner, repo, runID) @@ -1256,11 +1102,6 @@ func GetWorkflowRunUsage(getClient GetClientFn, t translations.TranslationHelper } defer func() { _ = resp.Body.Close() }() - r, err := json.Marshal(usage) - if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) - } - - return mcp.NewToolResultText(string(r)), nil + return MarshalledTextResult(usage), nil } } diff --git a/pkg/github/code_scanning.go b/pkg/github/code_scanning.go index 3b07692c0..24f503a29 100644 --- a/pkg/github/code_scanning.go +++ b/pkg/github/code_scanning.go @@ -2,12 +2,7 @@ package github import ( "context" - "encoding/json" - "fmt" - "io" - "net/http" - ghErrors "github.com/github/github-mcp-server/pkg/errors" "github.com/github/github-mcp-server/pkg/translations" "github.com/google/go-github/v72/github" "github.com/mark3labs/mcp-go/mcp" @@ -21,62 +16,25 @@ func GetCodeScanningAlert(getClient GetClientFn, t translations.TranslationHelpe Title: t("TOOL_GET_CODE_SCANNING_ALERT_USER_TITLE", "Get code scanning alert"), ReadOnlyHint: ToBoolPtr(true), }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description("The owner of the repository."), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("The name of the repository."), - ), - mcp.WithNumber("alertNumber", - mcp.Required(), - mcp.Description("The number of the alert."), - ), + WithOwnerParam(), + WithRepoParam(), + WithAlertNumberParam(), ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := RequiredParam[string](request, "owner") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - repo, err := RequiredParam[string](request, "repo") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - alertNumber, err := RequiredInt(request, "alertNumber") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - - client, err := getClient(ctx) - if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) - } - - alert, resp, err := client.CodeScanning.GetAlert(ctx, owner, repo, int64(alertNumber)) - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - "failed to get alert", - resp, - err, - ), nil - } - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - return mcp.NewToolResultError(fmt.Sprintf("failed to get alert: %s", string(body))), nil - } - - r, err := json.Marshal(alert) - if err != nil { - return nil, fmt.Errorf("failed to marshal alert: %w", err) - } - - return mcp.NewToolResultText(string(r)), nil + return ExecuteWithClientAndValidation( + ctx, + getClient, + request, + func(req mcp.CallToolRequest) error { + _, _, _, err := ValidateOwnerRepoAlert(req) + return err + }, + func(ctx context.Context, client *github.Client) (*github.Alert, *github.Response, error) { + owner, repo, alertNumber, _ := ValidateOwnerRepoAlert(request) + return client.CodeScanning.GetAlert(ctx, owner, repo, int64(alertNumber)) + }, + "get alert", + ) } } @@ -87,14 +45,8 @@ func ListCodeScanningAlerts(getClient GetClientFn, t translations.TranslationHel Title: t("TOOL_LIST_CODE_SCANNING_ALERTS_USER_TITLE", "List code scanning alerts"), ReadOnlyHint: ToBoolPtr(true), }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description("The owner of the repository."), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("The name of the repository."), - ), + WithOwnerParam(), + WithRepoParam(), mcp.WithString("state", mcp.Description("Filter code scanning alerts by state. Defaults to open"), mcp.DefaultString("open"), @@ -112,58 +64,23 @@ func ListCodeScanningAlerts(getClient GetClientFn, t translations.TranslationHel ), ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := RequiredParam[string](request, "owner") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - repo, err := RequiredParam[string](request, "repo") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - ref, err := OptionalParam[string](request, "ref") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - state, err := OptionalParam[string](request, "state") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - severity, err := OptionalParam[string](request, "severity") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - toolName, err := OptionalParam[string](request, "tool_name") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - - client, err := getClient(ctx) - if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) - } - alerts, resp, err := client.CodeScanning.ListAlertsForRepo(ctx, owner, repo, &github.AlertListOptions{Ref: ref, State: state, Severity: severity, ToolName: toolName}) - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - "failed to list alerts", - resp, - err, - ), nil - } - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - return mcp.NewToolResultError(fmt.Sprintf("failed to list alerts: %s", string(body))), nil - } - - r, err := json.Marshal(alerts) - if err != nil { - return nil, fmt.Errorf("failed to marshal alerts: %w", err) - } - - return mcp.NewToolResultText(string(r)), nil + return ExecuteWithClientAndValidation( + ctx, + getClient, + request, + func(req mcp.CallToolRequest) error { + _, _, err := ValidateOwnerRepo(req) + return err + }, + func(ctx context.Context, client *github.Client) ([]*github.Alert, *github.Response, error) { + owner, repo, _ := ValidateOwnerRepo(request) + ref, _ := OptionalParam[string](request, "ref") + state, _ := OptionalParam[string](request, "state") + severity, _ := OptionalParam[string](request, "severity") + toolName, _ := OptionalParam[string](request, "tool_name") + return client.CodeScanning.ListAlertsForRepo(ctx, owner, repo, &github.AlertListOptions{Ref: ref, State: state, Severity: severity, ToolName: toolName}) + }, + "list alerts", + ) } } diff --git a/pkg/github/discussions.go b/pkg/github/discussions.go index a7ec8e20f..f8b1ecec7 100644 --- a/pkg/github/discussions.go +++ b/pkg/github/discussions.go @@ -20,14 +20,8 @@ func ListDiscussions(getGQLClient GetGQLClientFn, t translations.TranslationHelp Title: t("TOOL_LIST_DISCUSSIONS_USER_TITLE", "List discussions"), ReadOnlyHint: ToBoolPtr(true), }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description("Repository owner"), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("Repository name"), - ), + WithOwnerParam(), + WithRepoParam(), mcp.WithString("category", mcp.Description("Optional filter by discussion category ID. If provided, only discussions with this category are listed."), ), @@ -51,7 +45,7 @@ func ListDiscussions(getGQLClient GetGQLClientFn, t translations.TranslationHelp client, err := getGQLClient(ctx) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("failed to get GitHub GQL client: %v", err)), nil + return mcp.NewToolResultError(fmt.Errorf(ErrFailedToGetGitHubClient, err).Error()), nil } // If category filter is specified, use it as the category ID for server-side filtering @@ -162,18 +156,9 @@ func GetDiscussion(getGQLClient GetGQLClientFn, t translations.TranslationHelper Title: t("TOOL_GET_DISCUSSION_USER_TITLE", "Get discussion"), ReadOnlyHint: ToBoolPtr(true), }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description("Repository owner"), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("Repository name"), - ), - mcp.WithNumber("discussionNumber", - mcp.Required(), - mcp.Description("Discussion Number"), - ), + WithOwnerParam(), + WithRepoParam(), + WithDiscussionNumberParam(), ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { // Decode params @@ -187,7 +172,7 @@ func GetDiscussion(getGQLClient GetGQLClientFn, t translations.TranslationHelper } client, err := getGQLClient(ctx) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("failed to get GitHub GQL client: %v", err)), nil + return mcp.NewToolResultError(fmt.Errorf(ErrFailedToGetGitHubClient, err).Error()), nil } var q struct { @@ -241,9 +226,9 @@ func GetDiscussionComments(getGQLClient GetGQLClientFn, t translations.Translati Title: t("TOOL_GET_DISCUSSION_COMMENTS_USER_TITLE", "Get discussion comments"), ReadOnlyHint: ToBoolPtr(true), }), - mcp.WithString("owner", mcp.Required(), mcp.Description("Repository owner")), - mcp.WithString("repo", mcp.Required(), mcp.Description("Repository name")), - mcp.WithNumber("discussionNumber", mcp.Required(), mcp.Description("Discussion Number")), + WithOwnerParam(), + WithRepoParam(), + WithDiscussionNumberParam(), ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { // Decode params @@ -258,7 +243,7 @@ func GetDiscussionComments(getGQLClient GetGQLClientFn, t translations.Translati client, err := getGQLClient(ctx) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("failed to get GitHub GQL client: %v", err)), nil + return mcp.NewToolResultError(fmt.Errorf(ErrFailedToGetGitHubClient, err).Error()), nil } var q struct { @@ -301,14 +286,8 @@ func ListDiscussionCategories(getGQLClient GetGQLClientFn, t translations.Transl Title: t("TOOL_LIST_DISCUSSION_CATEGORIES_USER_TITLE", "List discussion categories"), ReadOnlyHint: ToBoolPtr(true), }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description("Repository owner"), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("Repository name"), - ), + WithOwnerParam(), + WithRepoParam(), mcp.WithNumber("first", mcp.Description("Number of categories to return per page (min 1, max 100)"), mcp.Min(1), @@ -356,7 +335,7 @@ func ListDiscussionCategories(getGQLClient GetGQLClientFn, t translations.Transl client, err := getGQLClient(ctx) if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("failed to get GitHub GQL client: %v", err)), nil + return mcp.NewToolResultError(fmt.Errorf(ErrFailedToGetGitHubClient, err).Error()), nil } var q struct { Repository struct { diff --git a/pkg/github/issues.go b/pkg/github/issues.go index 6121786d2..dc40c702b 100644 --- a/pkg/github/issues.go +++ b/pkg/github/issues.go @@ -2,9 +2,7 @@ package github import ( "context" - "encoding/json" "fmt" - "io" "net/http" "strings" "time" @@ -25,18 +23,9 @@ func GetIssue(getClient GetClientFn, t translations.TranslationHelperFunc) (tool Title: t("TOOL_GET_ISSUE_USER_TITLE", "Get issue details"), ReadOnlyHint: ToBoolPtr(true), }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description("The owner of the repository"), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("The name of the repository"), - ), - mcp.WithNumber("issue_number", - mcp.Required(), - mcp.Description("The number of the issue"), - ), + WithOwnerParam(), + WithRepoParam(), + WithIssueNumberParam(), ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { owner, err := RequiredParam[string](request, "owner") @@ -54,28 +43,14 @@ func GetIssue(getClient GetClientFn, t translations.TranslationHelperFunc) (tool client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(ErrFailedToGetGitHubClient, err) } issue, resp, err := client.Issues.Get(ctx, owner, repo, issueNumber) if err != nil { return nil, fmt.Errorf("failed to get issue: %w", err) } defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - return mcp.NewToolResultError(fmt.Sprintf("failed to get issue: %s", string(body))), nil - } - - r, err := json.Marshal(issue) - if err != nil { - return nil, fmt.Errorf("failed to marshal issue: %w", err) - } - - return mcp.NewToolResultText(string(r)), nil + return MarshalledTextResult(issue), nil } } @@ -87,18 +62,9 @@ func AddIssueComment(getClient GetClientFn, t translations.TranslationHelperFunc Title: t("TOOL_ADD_ISSUE_COMMENT_USER_TITLE", "Add comment to issue"), ReadOnlyHint: ToBoolPtr(false), }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description("Repository owner"), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("Repository name"), - ), - mcp.WithNumber("issue_number", - mcp.Required(), - mcp.Description("Issue number to comment on"), - ), + WithOwnerParam(), + WithRepoParam(), + WithIssueNumberParam(), mcp.WithString("body", mcp.Required(), mcp.Description("Comment content"), @@ -128,7 +94,7 @@ func AddIssueComment(getClient GetClientFn, t translations.TranslationHelperFunc client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(ErrFailedToGetGitHubClient, err) } createdComment, resp, err := client.Issues.CreateComment(ctx, owner, repo, issueNumber, comment) if err != nil { @@ -137,19 +103,10 @@ func AddIssueComment(getClient GetClientFn, t translations.TranslationHelperFunc defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusCreated { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - return mcp.NewToolResultError(fmt.Sprintf("failed to create comment: %s", string(body))), nil + return HandleHTTPError(resp, "create comment") } - r, err := json.Marshal(createdComment) - if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) - } - - return mcp.NewToolResultText(string(r)), nil + return MarshalledTextResult(createdComment), nil } } @@ -206,14 +163,8 @@ func CreateIssue(getClient GetClientFn, t translations.TranslationHelperFunc) (t Title: t("TOOL_CREATE_ISSUE_USER_TITLE", "Open new issue"), ReadOnlyHint: ToBoolPtr(false), }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description("Repository owner"), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("Repository name"), - ), + WithOwnerParam(), + WithRepoParam(), mcp.WithString("title", mcp.Required(), mcp.Description("Issue title"), @@ -295,7 +246,7 @@ func CreateIssue(getClient GetClientFn, t translations.TranslationHelperFunc) (t client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(ErrFailedToGetGitHubClient, err) } issue, resp, err := client.Issues.Create(ctx, owner, repo, issueRequest) if err != nil { @@ -304,19 +255,10 @@ func CreateIssue(getClient GetClientFn, t translations.TranslationHelperFunc) (t defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusCreated { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - return mcp.NewToolResultError(fmt.Sprintf("failed to create issue: %s", string(body))), nil + return HandleHTTPError(resp, "create issue") } - r, err := json.Marshal(issue) - if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) - } - - return mcp.NewToolResultText(string(r)), nil + return MarshalledTextResult(issue), nil } } @@ -328,14 +270,8 @@ func ListIssues(getClient GetClientFn, t translations.TranslationHelperFunc) (to Title: t("TOOL_LIST_ISSUES_USER_TITLE", "List issues"), ReadOnlyHint: ToBoolPtr(true), }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description("Repository owner"), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("Repository name"), - ), + WithOwnerParam(), + WithRepoParam(), mcp.WithString("state", mcp.Description("Filter by state"), mcp.Enum("open", "closed", "all"), @@ -417,7 +353,7 @@ func ListIssues(getClient GetClientFn, t translations.TranslationHelperFunc) (to client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(ErrFailedToGetGitHubClient, err) } issues, resp, err := client.Issues.ListByRepo(ctx, owner, repo, opts) if err != nil { @@ -426,19 +362,10 @@ func ListIssues(getClient GetClientFn, t translations.TranslationHelperFunc) (to defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - return mcp.NewToolResultError(fmt.Sprintf("failed to list issues: %s", string(body))), nil + return HandleHTTPError(resp, "list issues") } - r, err := json.Marshal(issues) - if err != nil { - return nil, fmt.Errorf("failed to marshal issues: %w", err) - } - - return mcp.NewToolResultText(string(r)), nil + return MarshalledTextResult(issues), nil } } @@ -450,18 +377,9 @@ func UpdateIssue(getClient GetClientFn, t translations.TranslationHelperFunc) (t Title: t("TOOL_UPDATE_ISSUE_USER_TITLE", "Edit issue"), ReadOnlyHint: ToBoolPtr(false), }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description("Repository owner"), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("Repository name"), - ), - mcp.WithNumber("issue_number", - mcp.Required(), - mcp.Description("Issue number to update"), - ), + WithOwnerParam(), + WithRepoParam(), + WithIssueNumberParam(), mcp.WithString("title", mcp.Description("New title"), ), @@ -563,7 +481,7 @@ func UpdateIssue(getClient GetClientFn, t translations.TranslationHelperFunc) (t client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(ErrFailedToGetGitHubClient, err) } updatedIssue, resp, err := client.Issues.Edit(ctx, owner, repo, issueNumber, issueRequest) if err != nil { @@ -572,19 +490,10 @@ func UpdateIssue(getClient GetClientFn, t translations.TranslationHelperFunc) (t defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - return mcp.NewToolResultError(fmt.Sprintf("failed to update issue: %s", string(body))), nil - } - - r, err := json.Marshal(updatedIssue) - if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) + return HandleHTTPError(resp, "update issue") } - return mcp.NewToolResultText(string(r)), nil + return MarshalledTextResult(updatedIssue), nil } } @@ -596,18 +505,9 @@ func GetIssueComments(getClient GetClientFn, t translations.TranslationHelperFun Title: t("TOOL_GET_ISSUE_COMMENTS_USER_TITLE", "Get issue comments"), ReadOnlyHint: ToBoolPtr(true), }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description("Repository owner"), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("Repository name"), - ), - mcp.WithNumber("issue_number", - mcp.Required(), - mcp.Description("Issue number"), - ), + WithOwnerParam(), + WithRepoParam(), + WithIssueNumberParam(), mcp.WithNumber("page", mcp.Description("Page number"), ), @@ -646,7 +546,7 @@ func GetIssueComments(getClient GetClientFn, t translations.TranslationHelperFun client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(ErrFailedToGetGitHubClient, err) } comments, resp, err := client.Issues.ListComments(ctx, owner, repo, issueNumber, opts) if err != nil { @@ -655,19 +555,10 @@ func GetIssueComments(getClient GetClientFn, t translations.TranslationHelperFun defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - return mcp.NewToolResultError(fmt.Sprintf("failed to get issue comments: %s", string(body))), nil + return HandleHTTPError(resp, "get issue comments") } - r, err := json.Marshal(comments) - if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) - } - - return mcp.NewToolResultText(string(r)), nil + return MarshalledTextResult(comments), nil } } @@ -719,18 +610,9 @@ func AssignCopilotToIssue(getGQLClient GetGQLClientFn, t translations.Translatio ReadOnlyHint: ToBoolPtr(false), IdempotentHint: ToBoolPtr(true), }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description("Repository owner"), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("Repository name"), - ), - mcp.WithNumber("issueNumber", - mcp.Required(), - mcp.Description("Issue number"), - ), + WithOwnerParam(), + WithRepoParam(), + WithIssueNumberParamAlt(), ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { var params struct { @@ -744,7 +626,7 @@ func AssignCopilotToIssue(getGQLClient GetGQLClientFn, t translations.Translatio client, err := getGQLClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(ErrFailedToGetGitHubClient, err) } // Firstly, we try to find the copilot bot in the suggested actors for the repository. diff --git a/pkg/github/notifications.go b/pkg/github/notifications.go index b6b6bfd79..4b3c420e5 100644 --- a/pkg/github/notifications.go +++ b/pkg/github/notifications.go @@ -2,7 +2,6 @@ package github import ( "context" - "encoding/json" "fmt" "io" "net/http" @@ -22,6 +21,83 @@ const ( FilterOnlyParticipating = "only_participating" ) +func validateListNotificationsParams(request mcp.CallToolRequest) (*github.NotificationListOptions, error) { + filter, err := OptionalParam[string](request, "filter") + if err != nil { + return nil, err + } + + since, err := OptionalParam[string](request, "since") + if err != nil { + return nil, err + } + + before, err := OptionalParam[string](request, "before") + if err != nil { + return nil, err + } + + paginationParams, err := OptionalPaginationParams(request) + if err != nil { + return nil, err + } + + // Build options + opts := &github.NotificationListOptions{ + All: filter == FilterIncludeRead, + Participating: filter == FilterOnlyParticipating, + ListOptions: github.ListOptions{ + Page: paginationParams.page, + PerPage: paginationParams.perPage, + }, + } + + // Parse time parameters if provided + if since != "" { + sinceTime, err := time.Parse(time.RFC3339, since) + if err != nil { + return nil, fmt.Errorf("invalid since time format, should be RFC3339/ISO8601: %v", err) + } + opts.Since = sinceTime + } + + if before != "" { + beforeTime, err := time.Parse(time.RFC3339, before) + if err != nil { + return nil, fmt.Errorf("invalid before time format, should be RFC3339/ISO8601: %v", err) + } + opts.Before = beforeTime + } + + return opts, nil +} + +func fetchNotifications(ctx context.Context, client *github.Client, opts *github.NotificationListOptions, owner, repo string) ([]*github.Notification, *github.Response, error) { + var notifications []*github.Notification + var resp *github.Response + var err error + + if owner != "" && repo != "" { + notifications, resp, err = client.Activity.ListRepositoryNotifications(ctx, owner, repo, opts) + } else { + notifications, resp, err = client.Activity.ListNotifications(ctx, opts) + } + + if err != nil { + return nil, resp, err + } + + if resp.StatusCode != http.StatusOK { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, resp, fmt.Errorf(ErrFailedToReadResponseBody, err) + } + return nil, resp, fmt.Errorf("failed to get notifications: %s", string(body)) + } + + return notifications, resp, nil +} + // ListNotifications creates a tool to list notifications for the current user. func ListNotifications(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("list_notifications", @@ -51,20 +127,10 @@ func ListNotifications(getClient GetClientFn, t translations.TranslationHelperFu func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) - } - - filter, err := OptionalParam[string](request, "filter") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - - since, err := OptionalParam[string](request, "since") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return nil, fmt.Errorf(ErrFailedToGetGitHubClient, err) } - before, err := OptionalParam[string](request, "before") + opts, err := validateListNotificationsParams(request) if err != nil { return mcp.NewToolResultError(err.Error()), nil } @@ -78,46 +144,7 @@ func ListNotifications(getClient GetClientFn, t translations.TranslationHelperFu return mcp.NewToolResultError(err.Error()), nil } - paginationParams, err := OptionalPaginationParams(request) - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - - // Build options - opts := &github.NotificationListOptions{ - All: filter == FilterIncludeRead, - Participating: filter == FilterOnlyParticipating, - ListOptions: github.ListOptions{ - Page: paginationParams.page, - PerPage: paginationParams.perPage, - }, - } - - // Parse time parameters if provided - if since != "" { - sinceTime, err := time.Parse(time.RFC3339, since) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("invalid since time format, should be RFC3339/ISO8601: %v", err)), nil - } - opts.Since = sinceTime - } - - if before != "" { - beforeTime, err := time.Parse(time.RFC3339, before) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("invalid before time format, should be RFC3339/ISO8601: %v", err)), nil - } - opts.Before = beforeTime - } - - var notifications []*github.Notification - var resp *github.Response - - if owner != "" && repo != "" { - notifications, resp, err = client.Activity.ListRepositoryNotifications(ctx, owner, repo, opts) - } else { - notifications, resp, err = client.Activity.ListNotifications(ctx, opts) - } + notifications, resp, err := fetchNotifications(ctx, client, opts, owner, repo) if err != nil { return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to list notifications", @@ -127,21 +154,7 @@ func ListNotifications(getClient GetClientFn, t translations.TranslationHelperFu } defer func() { _ = resp.Body.Close() }() - if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - return mcp.NewToolResultError(fmt.Sprintf("failed to get notifications: %s", string(body))), nil - } - - // Marshal response to JSON - r, err := json.Marshal(notifications) - if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) - } - - return mcp.NewToolResultText(string(r)), nil + return MarshalledTextResult(notifications), nil } } @@ -162,7 +175,7 @@ func DismissNotification(getclient GetClientFn, t translations.TranslationHelper func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { client, err := getclient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(ErrFailedToGetGitHubClient, err) } threadID, err := RequiredParam[string](request, "threadID") @@ -201,11 +214,7 @@ func DismissNotification(getclient GetClientFn, t translations.TranslationHelper defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusResetContent && resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - return mcp.NewToolResultError(fmt.Sprintf("failed to mark notification as %s: %s", state, string(body))), nil + return HandleHTTPError(resp, fmt.Sprintf("mark notification as %s", state)) } return mcp.NewToolResultText(fmt.Sprintf("Notification marked as %s", state)), nil @@ -233,7 +242,7 @@ func MarkAllNotificationsRead(getClient GetClientFn, t translations.TranslationH func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(ErrFailedToGetGitHubClient, err) } lastReadAt, err := OptionalParam[string](request, "lastReadAt") @@ -280,11 +289,7 @@ func MarkAllNotificationsRead(getClient GetClientFn, t translations.TranslationH defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusResetContent && resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - return mcp.NewToolResultError(fmt.Sprintf("failed to mark all notifications as read: %s", string(body))), nil + return HandleHTTPError(resp, "mark all notifications as read") } return mcp.NewToolResultText("All notifications marked as read"), nil @@ -307,7 +312,7 @@ func GetNotificationDetails(getClient GetClientFn, t translations.TranslationHel func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(ErrFailedToGetGitHubClient, err) } notificationID, err := RequiredParam[string](request, "notificationID") @@ -326,19 +331,10 @@ func GetNotificationDetails(getClient GetClientFn, t translations.TranslationHel defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - return mcp.NewToolResultError(fmt.Sprintf("failed to get notification details: %s", string(body))), nil - } - - r, err := json.Marshal(thread) - if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) + return HandleHTTPError(resp, "get notification details") } - return mcp.NewToolResultText(string(r)), nil + return MarshalledTextResult(thread), nil } } @@ -370,7 +366,7 @@ func ManageNotificationSubscription(getClient GetClientFn, t translations.Transl func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(ErrFailedToGetGitHubClient, err) } notificationID, err := RequiredParam[string](request, "notificationID") @@ -411,8 +407,7 @@ func ManageNotificationSubscription(getClient GetClientFn, t translations.Transl defer func() { _ = resp.Body.Close() }() if resp.StatusCode < 200 || resp.StatusCode >= 300 { - body, _ := io.ReadAll(resp.Body) - return mcp.NewToolResultError(fmt.Sprintf("failed to %s notification subscription: %s", action, string(body))), nil + return HandleHTTPError(resp, fmt.Sprintf("%s notification subscription", action)) } if action == NotificationActionDelete { @@ -420,11 +415,7 @@ func ManageNotificationSubscription(getClient GetClientFn, t translations.Transl return mcp.NewToolResultText("Notification subscription deleted"), nil } - r, err := json.Marshal(result) - if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) - } - return mcp.NewToolResultText(string(r)), nil + return MarshalledTextResult(result), nil } } @@ -434,6 +425,56 @@ const ( RepositorySubscriptionActionDelete = "delete" ) +func validateRepositorySubscriptionParams(request mcp.CallToolRequest) (string, string, string, error) { + owner, err := RequiredParam[string](request, "owner") + if err != nil { + return "", "", "", err + } + repo, err := RequiredParam[string](request, "repo") + if err != nil { + return "", "", "", err + } + action, err := RequiredParam[string](request, "action") + if err != nil { + return "", "", "", err + } + return owner, repo, action, nil +} + +func performRepositorySubscriptionAction(ctx context.Context, client *github.Client, owner, repo, action string) (any, *github.Response, error) { + switch action { + case RepositorySubscriptionActionIgnore: + sub := &github.Subscription{Ignored: ToBoolPtr(true)} + return client.Activity.SetRepositorySubscription(ctx, owner, repo, sub) + case RepositorySubscriptionActionWatch: + sub := &github.Subscription{Ignored: ToBoolPtr(false), Subscribed: ToBoolPtr(true)} + return client.Activity.SetRepositorySubscription(ctx, owner, repo, sub) + case RepositorySubscriptionActionDelete: + resp, err := client.Activity.DeleteRepositorySubscription(ctx, owner, repo) + return nil, resp, err + default: + return nil, nil, fmt.Errorf("invalid action. Must be one of: ignore, watch, delete") + } +} + +func handleRepositorySubscriptionResponse(action string, result any, resp *github.Response) (*mcp.CallToolResult, error) { + if resp != nil { + defer func() { _ = resp.Body.Close() }() + } + + // Handle non-2xx status codes + if resp != nil && (resp.StatusCode < 200 || resp.StatusCode >= 300) { + body, _ := io.ReadAll(resp.Body) + return mcp.NewToolResultError(fmt.Sprintf("failed to %s repository subscription: %s", action, string(body))), nil + } + + if action == RepositorySubscriptionActionDelete { + return mcp.NewToolResultText("Repository subscription deleted"), nil + } + + return MarshalledTextResult(result), nil +} + // ManageRepositoryNotificationSubscription creates a tool to manage a repository notification subscription (ignore, watch, delete) func ManageRepositoryNotificationSubscription(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("manage_repository_notification_subscription", @@ -459,41 +500,15 @@ func ManageRepositoryNotificationSubscription(getClient GetClientFn, t translati func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(ErrFailedToGetGitHubClient, err) } - owner, err := RequiredParam[string](request, "owner") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - repo, err := RequiredParam[string](request, "repo") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - action, err := RequiredParam[string](request, "action") + owner, repo, action, err := validateRepositorySubscriptionParams(request) if err != nil { return mcp.NewToolResultError(err.Error()), nil } - var ( - resp *github.Response - result any - apiErr error - ) - - switch action { - case RepositorySubscriptionActionIgnore: - sub := &github.Subscription{Ignored: ToBoolPtr(true)} - result, resp, apiErr = client.Activity.SetRepositorySubscription(ctx, owner, repo, sub) - case RepositorySubscriptionActionWatch: - sub := &github.Subscription{Ignored: ToBoolPtr(false), Subscribed: ToBoolPtr(true)} - result, resp, apiErr = client.Activity.SetRepositorySubscription(ctx, owner, repo, sub) - case RepositorySubscriptionActionDelete: - resp, apiErr = client.Activity.DeleteRepositorySubscription(ctx, owner, repo) - default: - return mcp.NewToolResultError("Invalid action. Must be one of: ignore, watch, delete."), nil - } - + result, resp, apiErr := performRepositorySubscriptionAction(ctx, client, owner, repo, action) if apiErr != nil { return ghErrors.NewGitHubAPIErrorResponse(ctx, fmt.Sprintf("failed to %s repository subscription", action), @@ -501,25 +516,7 @@ func ManageRepositoryNotificationSubscription(getClient GetClientFn, t translati apiErr, ), nil } - if resp != nil { - defer func() { _ = resp.Body.Close() }() - } - - // Handle non-2xx status codes - if resp != nil && (resp.StatusCode < 200 || resp.StatusCode >= 300) { - body, _ := io.ReadAll(resp.Body) - return mcp.NewToolResultError(fmt.Sprintf("failed to %s repository subscription: %s", action, string(body))), nil - } - if action == RepositorySubscriptionActionDelete { - // Special case for delete as there is no response body - return mcp.NewToolResultText("Repository subscription deleted"), nil - } - - r, err := json.Marshal(result) - if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) - } - return mcp.NewToolResultText(string(r)), nil + return handleRepositorySubscriptionResponse(action, result, resp) } } diff --git a/pkg/github/pullrequests.go b/pkg/github/pullrequests.go index bad822b13..7dbe48504 100644 --- a/pkg/github/pullrequests.go +++ b/pkg/github/pullrequests.go @@ -25,18 +25,9 @@ func GetPullRequest(getClient GetClientFn, t translations.TranslationHelperFunc) Title: t("TOOL_GET_PULL_REQUEST_USER_TITLE", "Get pull request details"), ReadOnlyHint: ToBoolPtr(true), }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description("Repository owner"), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("Repository name"), - ), - mcp.WithNumber("pullNumber", - mcp.Required(), - mcp.Description("Pull request number"), - ), + WithOwnerParam(), + WithRepoParam(), + WithPullNumberParam(), ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { owner, err := RequiredParam[string](request, "owner") @@ -54,7 +45,7 @@ func GetPullRequest(getClient GetClientFn, t translations.TranslationHelperFunc) client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(ErrFailedToGetGitHubClient, err) } pr, resp, err := client.PullRequests.Get(ctx, owner, repo, pullNumber) if err != nil { @@ -67,19 +58,10 @@ func GetPullRequest(getClient GetClientFn, t translations.TranslationHelperFunc) defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - return mcp.NewToolResultError(fmt.Sprintf("failed to get pull request: %s", string(body))), nil - } - - r, err := json.Marshal(pr) - if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) + return HandleHTTPError(resp, "get pull request") } - return mcp.NewToolResultText(string(r)), nil + return MarshalledTextResult(pr), nil } } @@ -91,14 +73,8 @@ func CreatePullRequest(getClient GetClientFn, t translations.TranslationHelperFu Title: t("TOOL_CREATE_PULL_REQUEST_USER_TITLE", "Open new pull request"), ReadOnlyHint: ToBoolPtr(false), }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description("Repository owner"), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("Repository name"), - ), + WithOwnerParam(), + WithRepoParam(), mcp.WithString("title", mcp.Required(), mcp.Description("PR title"), @@ -173,7 +149,7 @@ func CreatePullRequest(getClient GetClientFn, t translations.TranslationHelperFu client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(ErrFailedToGetGitHubClient, err) } pr, resp, err := client.PullRequests.Create(ctx, owner, repo, newPR) if err != nil { @@ -186,19 +162,10 @@ func CreatePullRequest(getClient GetClientFn, t translations.TranslationHelperFu defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusCreated { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - return mcp.NewToolResultError(fmt.Sprintf("failed to create pull request: %s", string(body))), nil + return HandleHTTPError(resp, "create pull request") } - r, err := json.Marshal(pr) - if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) - } - - return mcp.NewToolResultText(string(r)), nil + return MarshalledTextResult(pr), nil } } @@ -210,18 +177,9 @@ func UpdatePullRequest(getClient GetClientFn, t translations.TranslationHelperFu Title: t("TOOL_UPDATE_PULL_REQUEST_USER_TITLE", "Edit pull request"), ReadOnlyHint: ToBoolPtr(false), }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description("Repository owner"), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("Repository name"), - ), - mcp.WithNumber("pullNumber", - mcp.Required(), - mcp.Description("Pull request number to update"), - ), + WithOwnerParam(), + WithRepoParam(), + WithPullNumberParam(), mcp.WithString("title", mcp.Description("New title"), ), @@ -298,7 +256,7 @@ func UpdatePullRequest(getClient GetClientFn, t translations.TranslationHelperFu client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(ErrFailedToGetGitHubClient, err) } pr, resp, err := client.PullRequests.Edit(ctx, owner, repo, pullNumber, update) if err != nil { @@ -311,19 +269,10 @@ func UpdatePullRequest(getClient GetClientFn, t translations.TranslationHelperFu defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - return mcp.NewToolResultError(fmt.Sprintf("failed to update pull request: %s", string(body))), nil - } - - r, err := json.Marshal(pr) - if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) + return HandleHTTPError(resp, "update pull request") } - return mcp.NewToolResultText(string(r)), nil + return MarshalledTextResult(pr), nil } } @@ -335,14 +284,8 @@ func ListPullRequests(getClient GetClientFn, t translations.TranslationHelperFun Title: t("TOOL_LIST_PULL_REQUESTS_USER_TITLE", "List pull requests"), ReadOnlyHint: ToBoolPtr(true), }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description("Repository owner"), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("Repository name"), - ), + WithOwnerParam(), + WithRepoParam(), mcp.WithString("state", mcp.Description("Filter by state"), mcp.Enum("open", "closed", "all"), @@ -411,7 +354,7 @@ func ListPullRequests(getClient GetClientFn, t translations.TranslationHelperFun client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(ErrFailedToGetGitHubClient, err) } prs, resp, err := client.PullRequests.List(ctx, owner, repo, opts) if err != nil { @@ -424,19 +367,10 @@ func ListPullRequests(getClient GetClientFn, t translations.TranslationHelperFun defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - return mcp.NewToolResultError(fmt.Sprintf("failed to list pull requests: %s", string(body))), nil - } - - r, err := json.Marshal(prs) - if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) + return HandleHTTPError(resp, "list pull requests") } - return mcp.NewToolResultText(string(r)), nil + return MarshalledTextResult(prs), nil } } @@ -448,18 +382,9 @@ func MergePullRequest(getClient GetClientFn, t translations.TranslationHelperFun Title: t("TOOL_MERGE_PULL_REQUEST_USER_TITLE", "Merge pull request"), ReadOnlyHint: ToBoolPtr(false), }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description("Repository owner"), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("Repository name"), - ), - mcp.WithNumber("pullNumber", - mcp.Required(), - mcp.Description("Pull request number"), - ), + WithOwnerParam(), + WithRepoParam(), + WithPullNumberParam(), mcp.WithString("commit_title", mcp.Description("Title for merge commit"), ), @@ -504,7 +429,7 @@ func MergePullRequest(getClient GetClientFn, t translations.TranslationHelperFun client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(ErrFailedToGetGitHubClient, err) } result, resp, err := client.PullRequests.Merge(ctx, owner, repo, pullNumber, commitMessage, options) if err != nil { @@ -517,19 +442,10 @@ func MergePullRequest(getClient GetClientFn, t translations.TranslationHelperFun defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - return mcp.NewToolResultError(fmt.Sprintf("failed to merge pull request: %s", string(body))), nil - } - - r, err := json.Marshal(result) - if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) + return HandleHTTPError(resp, "merge pull request") } - return mcp.NewToolResultText(string(r)), nil + return MarshalledTextResult(result), nil } } @@ -586,18 +502,9 @@ func GetPullRequestFiles(getClient GetClientFn, t translations.TranslationHelper Title: t("TOOL_GET_PULL_REQUEST_FILES_USER_TITLE", "Get pull request files"), ReadOnlyHint: ToBoolPtr(true), }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description("Repository owner"), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("Repository name"), - ), - mcp.WithNumber("pullNumber", - mcp.Required(), - mcp.Description("Pull request number"), - ), + WithOwnerParam(), + WithRepoParam(), + WithPullNumberParam(), WithPagination(), ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { @@ -620,7 +527,7 @@ func GetPullRequestFiles(getClient GetClientFn, t translations.TranslationHelper client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(ErrFailedToGetGitHubClient, err) } opts := &github.ListOptions{ PerPage: pagination.perPage, @@ -639,14 +546,14 @@ func GetPullRequestFiles(getClient GetClientFn, t translations.TranslationHelper if resp.StatusCode != http.StatusOK { body, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) + return nil, fmt.Errorf(ErrFailedToReadResponseBody, err) } return mcp.NewToolResultError(fmt.Sprintf("failed to get pull request files: %s", string(body))), nil } r, err := json.Marshal(files) if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) + return nil, fmt.Errorf(ErrFailedToMarshalResponse, err) } return mcp.NewToolResultText(string(r)), nil @@ -661,18 +568,9 @@ func GetPullRequestStatus(getClient GetClientFn, t translations.TranslationHelpe Title: t("TOOL_GET_PULL_REQUEST_STATUS_USER_TITLE", "Get pull request status checks"), ReadOnlyHint: ToBoolPtr(true), }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description("Repository owner"), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("Repository name"), - ), - mcp.WithNumber("pullNumber", - mcp.Required(), - mcp.Description("Pull request number"), - ), + WithOwnerParam(), + WithRepoParam(), + WithPullNumberParam(), ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { owner, err := RequiredParam[string](request, "owner") @@ -690,7 +588,7 @@ func GetPullRequestStatus(getClient GetClientFn, t translations.TranslationHelpe // First get the PR to find the head SHA client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(ErrFailedToGetGitHubClient, err) } pr, resp, err := client.PullRequests.Get(ctx, owner, repo, pullNumber) if err != nil { @@ -703,11 +601,7 @@ func GetPullRequestStatus(getClient GetClientFn, t translations.TranslationHelpe defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - return mcp.NewToolResultError(fmt.Sprintf("failed to get pull request: %s", string(body))), nil + return HandleHTTPError(resp, "get pull request") } // Get combined status for the head SHA @@ -724,14 +618,14 @@ func GetPullRequestStatus(getClient GetClientFn, t translations.TranslationHelpe if resp.StatusCode != http.StatusOK { body, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) + return nil, fmt.Errorf(ErrFailedToReadResponseBody, err) } return mcp.NewToolResultError(fmt.Sprintf("failed to get combined status: %s", string(body))), nil } r, err := json.Marshal(status) if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) + return nil, fmt.Errorf(ErrFailedToMarshalResponse, err) } return mcp.NewToolResultText(string(r)), nil @@ -746,18 +640,9 @@ func UpdatePullRequestBranch(getClient GetClientFn, t translations.TranslationHe Title: t("TOOL_UPDATE_PULL_REQUEST_BRANCH_USER_TITLE", "Update pull request branch"), ReadOnlyHint: ToBoolPtr(false), }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description("Repository owner"), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("Repository name"), - ), - mcp.WithNumber("pullNumber", - mcp.Required(), - mcp.Description("Pull request number"), - ), + WithOwnerParam(), + WithRepoParam(), + WithPullNumberParam(), mcp.WithString("expectedHeadSha", mcp.Description("The expected SHA of the pull request's HEAD ref"), ), @@ -786,7 +671,7 @@ func UpdatePullRequestBranch(getClient GetClientFn, t translations.TranslationHe client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(ErrFailedToGetGitHubClient, err) } result, resp, err := client.PullRequests.UpdateBranch(ctx, owner, repo, pullNumber, opts) if err != nil { @@ -806,17 +691,12 @@ func UpdatePullRequestBranch(getClient GetClientFn, t translations.TranslationHe if resp.StatusCode != http.StatusAccepted { body, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) + return nil, fmt.Errorf(ErrFailedToReadResponseBody, err) } return mcp.NewToolResultError(fmt.Sprintf("failed to update pull request branch: %s", string(body))), nil } - r, err := json.Marshal(result) - if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) - } - - return mcp.NewToolResultText(string(r)), nil + return MarshalledTextResult(result), nil } } @@ -828,18 +708,9 @@ func GetPullRequestComments(getClient GetClientFn, t translations.TranslationHel Title: t("TOOL_GET_PULL_REQUEST_COMMENTS_USER_TITLE", "Get pull request comments"), ReadOnlyHint: ToBoolPtr(true), }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description("Repository owner"), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("Repository name"), - ), - mcp.WithNumber("pullNumber", - mcp.Required(), - mcp.Description("Pull request number"), - ), + WithOwnerParam(), + WithRepoParam(), + WithPullNumberParam(), ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { owner, err := RequiredParam[string](request, "owner") @@ -863,7 +734,7 @@ func GetPullRequestComments(getClient GetClientFn, t translations.TranslationHel client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(ErrFailedToGetGitHubClient, err) } comments, resp, err := client.PullRequests.ListComments(ctx, owner, repo, pullNumber, opts) if err != nil { @@ -878,14 +749,14 @@ func GetPullRequestComments(getClient GetClientFn, t translations.TranslationHel if resp.StatusCode != http.StatusOK { body, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) + return nil, fmt.Errorf(ErrFailedToReadResponseBody, err) } return mcp.NewToolResultError(fmt.Sprintf("failed to get pull request comments: %s", string(body))), nil } r, err := json.Marshal(comments) if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) + return nil, fmt.Errorf(ErrFailedToMarshalResponse, err) } return mcp.NewToolResultText(string(r)), nil @@ -900,18 +771,9 @@ func GetPullRequestReviews(getClient GetClientFn, t translations.TranslationHelp Title: t("TOOL_GET_PULL_REQUEST_REVIEWS_USER_TITLE", "Get pull request reviews"), ReadOnlyHint: ToBoolPtr(true), }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description("Repository owner"), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("Repository name"), - ), - mcp.WithNumber("pullNumber", - mcp.Required(), - mcp.Description("Pull request number"), - ), + WithOwnerParam(), + WithRepoParam(), + WithPullNumberParam(), ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { owner, err := RequiredParam[string](request, "owner") @@ -929,7 +791,7 @@ func GetPullRequestReviews(getClient GetClientFn, t translations.TranslationHelp client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(ErrFailedToGetGitHubClient, err) } reviews, resp, err := client.PullRequests.ListReviews(ctx, owner, repo, pullNumber, nil) if err != nil { @@ -942,19 +804,10 @@ func GetPullRequestReviews(getClient GetClientFn, t translations.TranslationHelp defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - return mcp.NewToolResultError(fmt.Sprintf("failed to get pull request reviews: %s", string(body))), nil - } - - r, err := json.Marshal(reviews) - if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) + return HandleHTTPError(resp, "get pull request reviews") } - return mcp.NewToolResultText(string(r)), nil + return MarshalledTextResult(reviews), nil } } @@ -965,21 +818,9 @@ func CreateAndSubmitPullRequestReview(getGQLClient GetGQLClientFn, t translation Title: t("TOOL_CREATE_AND_SUBMIT_PULL_REQUEST_REVIEW_USER_TITLE", "Create and submit a pull request review without comments"), ReadOnlyHint: ToBoolPtr(false), }), - // Either we need the PR GQL Id directly, or we need owner, repo and PR number to look it up. - // Since our other Pull Request tools are working with the REST Client, will handle the lookup - // internally for now. - mcp.WithString("owner", - mcp.Required(), - mcp.Description("Repository owner"), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("Repository name"), - ), - mcp.WithNumber("pullNumber", - mcp.Required(), - mcp.Description("Pull request number"), - ), + WithOwnerParam(), + WithRepoParam(), + WithPullNumberParam(), mcp.WithString("body", mcp.Required(), mcp.Description("Review comment text"), @@ -1068,21 +909,9 @@ func CreatePendingPullRequestReview(getGQLClient GetGQLClientFn, t translations. Title: t("TOOL_CREATE_PENDING_PULL_REQUEST_REVIEW_USER_TITLE", "Create pending pull request review"), ReadOnlyHint: ToBoolPtr(false), }), - // Either we need the PR GQL Id directly, or we need owner, repo and PR number to look it up. - // Since our other Pull Request tools are working with the REST Client, will handle the lookup - // internally for now. - mcp.WithString("owner", - mcp.Required(), - mcp.Description("Repository owner"), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("Repository name"), - ), - mcp.WithNumber("pullNumber", - mcp.Required(), - mcp.Description("Pull request number"), - ), + WithOwnerParam(), + WithRepoParam(), + WithPullNumberParam(), mcp.WithString("commitID", mcp.Description("SHA of commit to review"), ), @@ -1163,24 +992,9 @@ func AddPullRequestReviewCommentToPendingReview(getGQLClient GetGQLClientFn, t t // Ideally, for performance sake this would just accept the pullRequestReviewID. However, we would need to // add a new tool to get that ID for clients that aren't in the same context as the original pending review // creation. So for now, we'll just accept the owner, repo and pull number and assume this is adding a comment - // the latest review from a user, since only one can be active at a time. It can later be extended with - // a pullRequestReviewID parameter if targeting other reviews is desired: - // mcp.WithString("pullRequestReviewID", - // mcp.Required(), - // mcp.Description("The ID of the pull request review to add a comment to"), - // ), - mcp.WithString("owner", - mcp.Required(), - mcp.Description("Repository owner"), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("Repository name"), - ), - mcp.WithNumber("pullNumber", - mcp.Required(), - mcp.Description("Pull request number"), - ), + WithOwnerParam(), + WithRepoParam(), + WithPullNumberParam(), mcp.WithString("path", mcp.Required(), mcp.Description("The relative path to the file that necessitates a comment"), @@ -1326,22 +1140,9 @@ func SubmitPendingPullRequestReview(getGQLClient GetGQLClientFn, t translations. Title: t("TOOL_SUBMIT_PENDING_PULL_REQUEST_REVIEW_USER_TITLE", "Submit the requester's latest pending pull request review"), ReadOnlyHint: ToBoolPtr(false), }), - // Ideally, for performance sake this would just accept the pullRequestReviewID. However, we would need to - // add a new tool to get that ID for clients that aren't in the same context as the original pending review - // creation. So for now, we'll just accept the owner, repo and pull number and assume this is submitting - // the latest review from a user, since only one can be active at a time. - mcp.WithString("owner", - mcp.Required(), - mcp.Description("Repository owner"), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("Repository name"), - ), - mcp.WithNumber("pullNumber", - mcp.Required(), - mcp.Description("Pull request number"), - ), + WithOwnerParam(), + WithRepoParam(), + WithPullNumberParam(), mcp.WithString("event", mcp.Required(), mcp.Description("The event to perform"), @@ -1460,22 +1261,9 @@ func DeletePendingPullRequestReview(getGQLClient GetGQLClientFn, t translations. Title: t("TOOL_DELETE_PENDING_PULL_REQUEST_REVIEW_USER_TITLE", "Delete the requester's latest pending pull request review"), ReadOnlyHint: ToBoolPtr(false), }), - // Ideally, for performance sake this would just accept the pullRequestReviewID. However, we would need to - // add a new tool to get that ID for clients that aren't in the same context as the original pending review - // creation. So for now, we'll just accept the owner, repo and pull number and assume this is deleting - // the latest pending review from a user, since only one can be active at a time. - mcp.WithString("owner", - mcp.Required(), - mcp.Description("Repository owner"), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("Repository name"), - ), - mcp.WithNumber("pullNumber", - mcp.Required(), - mcp.Description("Pull request number"), - ), + WithOwnerParam(), + WithRepoParam(), + WithPullNumberParam(), ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { var params struct { @@ -1579,18 +1367,9 @@ func GetPullRequestDiff(getClient GetClientFn, t translations.TranslationHelperF Title: t("TOOL_GET_PULL_REQUEST_DIFF_USER_TITLE", "Get pull request diff"), ReadOnlyHint: ToBoolPtr(true), }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description("Repository owner"), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("Repository name"), - ), - mcp.WithNumber("pullNumber", - mcp.Required(), - mcp.Description("Pull request number"), - ), + WithOwnerParam(), + WithRepoParam(), + WithPullNumberParam(), ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { var params struct { @@ -1625,7 +1404,7 @@ func GetPullRequestDiff(getClient GetClientFn, t translations.TranslationHelperF if resp.StatusCode != http.StatusOK { body, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) + return nil, fmt.Errorf(ErrFailedToReadResponseBody, err) } return mcp.NewToolResultError(fmt.Sprintf("failed to get pull request diff: %s", string(body))), nil } @@ -1647,18 +1426,9 @@ func RequestCopilotReview(getClient GetClientFn, t translations.TranslationHelpe Title: t("TOOL_REQUEST_COPILOT_REVIEW_USER_TITLE", "Request Copilot review"), ReadOnlyHint: ToBoolPtr(false), }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description("Repository owner"), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("Repository name"), - ), - mcp.WithNumber("pullNumber", - mcp.Required(), - mcp.Description("Pull request number"), - ), + WithOwnerParam(), + WithRepoParam(), + WithPullNumberParam(), ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { owner, err := RequiredParam[string](request, "owner") @@ -1703,7 +1473,7 @@ func RequestCopilotReview(getClient GetClientFn, t translations.TranslationHelpe if resp.StatusCode != http.StatusCreated { body, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) + return nil, fmt.Errorf(ErrFailedToReadResponseBody, err) } return mcp.NewToolResultError(fmt.Sprintf("failed to request copilot review: %s", string(body))), nil } diff --git a/pkg/github/repositories.go b/pkg/github/repositories.go index 29f776a05..bae71a3d3 100644 --- a/pkg/github/repositories.go +++ b/pkg/github/repositories.go @@ -26,14 +26,8 @@ func GetCommit(getClient GetClientFn, t translations.TranslationHelperFunc) (too Title: t("TOOL_GET_COMMITS_USER_TITLE", "Get commit details"), ReadOnlyHint: ToBoolPtr(true), }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description("Repository owner"), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("Repository name"), - ), + WithOwnerParam(), + WithRepoParam(), mcp.WithString("sha", mcp.Required(), mcp.Description("Commit SHA, branch name, or tag name"), @@ -65,7 +59,7 @@ func GetCommit(getClient GetClientFn, t translations.TranslationHelperFunc) (too client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(ErrFailedToGetGitHubClient, err) } commit, resp, err := client.Repositories.GetCommit(ctx, owner, repo, sha, opts) if err != nil { @@ -80,14 +74,14 @@ func GetCommit(getClient GetClientFn, t translations.TranslationHelperFunc) (too if resp.StatusCode != 200 { body, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) + return nil, fmt.Errorf(ErrFailedToReadResponseBody, err) } return mcp.NewToolResultError(fmt.Sprintf("failed to get commit: %s", string(body))), nil } r, err := json.Marshal(commit) if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) + return nil, fmt.Errorf(ErrFailedToMarshalResponse, err) } return mcp.NewToolResultText(string(r)), nil @@ -102,14 +96,8 @@ func ListCommits(getClient GetClientFn, t translations.TranslationHelperFunc) (t Title: t("TOOL_LIST_COMMITS_USER_TITLE", "List commits"), ReadOnlyHint: ToBoolPtr(true), }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description("Repository owner"), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("Repository name"), - ), + WithOwnerParam(), + WithRepoParam(), mcp.WithString("sha", mcp.Description("The commit SHA, branch name, or tag name to list commits from. If not specified, defaults to the repository's default branch."), ), @@ -155,7 +143,7 @@ func ListCommits(getClient GetClientFn, t translations.TranslationHelperFunc) (t client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(ErrFailedToGetGitHubClient, err) } commits, resp, err := client.Repositories.ListCommits(ctx, owner, repo, opts) if err != nil { @@ -168,19 +156,10 @@ func ListCommits(getClient GetClientFn, t translations.TranslationHelperFunc) (t defer func() { _ = resp.Body.Close() }() if resp.StatusCode != 200 { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - return mcp.NewToolResultError(fmt.Sprintf("failed to list commits: %s", string(body))), nil + return HandleHTTPError(resp, "list commits") } - r, err := json.Marshal(commits) - if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) - } - - return mcp.NewToolResultText(string(r)), nil + return MarshalledTextResult(commits), nil } } @@ -192,14 +171,8 @@ func ListBranches(getClient GetClientFn, t translations.TranslationHelperFunc) ( Title: t("TOOL_LIST_BRANCHES_USER_TITLE", "List branches"), ReadOnlyHint: ToBoolPtr(true), }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description("Repository owner"), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("Repository name"), - ), + WithOwnerParam(), + WithRepoParam(), WithPagination(), ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { @@ -225,7 +198,7 @@ func ListBranches(getClient GetClientFn, t translations.TranslationHelperFunc) ( client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(ErrFailedToGetGitHubClient, err) } branches, resp, err := client.Repositories.ListBranches(ctx, owner, repo, opts) @@ -239,19 +212,10 @@ func ListBranches(getClient GetClientFn, t translations.TranslationHelperFunc) ( defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - return mcp.NewToolResultError(fmt.Sprintf("failed to list branches: %s", string(body))), nil + return HandleHTTPError(resp, "list branches") } - r, err := json.Marshal(branches) - if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) - } - - return mcp.NewToolResultText(string(r)), nil + return MarshalledTextResult(branches), nil } } @@ -263,14 +227,8 @@ func CreateOrUpdateFile(getClient GetClientFn, t translations.TranslationHelperF Title: t("TOOL_CREATE_OR_UPDATE_FILE_USER_TITLE", "Create or update file"), ReadOnlyHint: ToBoolPtr(false), }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description("Repository owner (username or organization)"), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("Repository name"), - ), + WithOwnerParam(), + WithRepoParam(), mcp.WithString("path", mcp.Required(), mcp.Description("Path where to create/update the file"), @@ -339,7 +297,7 @@ func CreateOrUpdateFile(getClient GetClientFn, t translations.TranslationHelperF // Create or update the file client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(ErrFailedToGetGitHubClient, err) } fileContent, resp, err := client.Repositories.CreateFile(ctx, owner, repo, path, opts) if err != nil { @@ -354,14 +312,14 @@ func CreateOrUpdateFile(getClient GetClientFn, t translations.TranslationHelperF if resp.StatusCode != 200 && resp.StatusCode != 201 { body, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) + return nil, fmt.Errorf(ErrFailedToReadResponseBody, err) } return mcp.NewToolResultError(fmt.Sprintf("failed to create/update file: %s", string(body))), nil } r, err := json.Marshal(fileContent) if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) + return nil, fmt.Errorf(ErrFailedToMarshalResponse, err) } return mcp.NewToolResultText(string(r)), nil @@ -417,7 +375,7 @@ func CreateRepository(getClient GetClientFn, t translations.TranslationHelperFun client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(ErrFailedToGetGitHubClient, err) } createdRepo, resp, err := client.Repositories.Create(ctx, "", repo) if err != nil { @@ -430,19 +388,10 @@ func CreateRepository(getClient GetClientFn, t translations.TranslationHelperFun defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusCreated { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - return mcp.NewToolResultError(fmt.Sprintf("failed to create repository: %s", string(body))), nil - } - - r, err := json.Marshal(createdRepo) - if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) + return HandleHTTPError(resp, "create repository") } - return mcp.NewToolResultText(string(r)), nil + return MarshalledTextResult(createdRepo), nil } } @@ -454,14 +403,8 @@ func GetFileContents(getClient GetClientFn, getRawClient raw.GetRawClientFn, t t Title: t("TOOL_GET_FILE_CONTENTS_USER_TITLE", "Get file or directory contents"), ReadOnlyHint: ToBoolPtr(true), }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description("Repository owner (username or organization)"), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("Repository name"), - ), + WithOwnerParam(), + WithRepoParam(), mcp.WithString("path", mcp.Required(), mcp.Description("Path to file/directory (directories must end with a slash '/')"), @@ -503,7 +446,7 @@ func GetFileContents(getClient GetClientFn, getRawClient raw.GetRawClientFn, t t // fetch the PR from the API to get the latest commit and use SHA githubClient, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(ErrFailedToGetGitHubClient, err) } prNum, err := strconv.Atoi(prNumber) if err != nil { @@ -622,14 +565,8 @@ func ForkRepository(getClient GetClientFn, t translations.TranslationHelperFunc) Title: t("TOOL_FORK_REPOSITORY_USER_TITLE", "Fork repository"), ReadOnlyHint: ToBoolPtr(false), }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description("Repository owner"), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("Repository name"), - ), + WithOwnerParam(), + WithRepoParam(), mcp.WithString("organization", mcp.Description("Organization to fork to"), ), @@ -655,7 +592,7 @@ func ForkRepository(getClient GetClientFn, t translations.TranslationHelperFunc) client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(ErrFailedToGetGitHubClient, err) } forkedRepo, resp, err := client.Repositories.CreateFork(ctx, owner, repo, opts) if err != nil { @@ -675,14 +612,14 @@ func ForkRepository(getClient GetClientFn, t translations.TranslationHelperFunc) if resp.StatusCode != http.StatusAccepted { body, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) + return nil, fmt.Errorf(ErrFailedToReadResponseBody, err) } return mcp.NewToolResultError(fmt.Sprintf("failed to fork repository: %s", string(body))), nil } r, err := json.Marshal(forkedRepo) if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) + return nil, fmt.Errorf(ErrFailedToMarshalResponse, err) } return mcp.NewToolResultText(string(r)), nil @@ -703,14 +640,8 @@ func DeleteFile(getClient GetClientFn, t translations.TranslationHelperFunc) (to ReadOnlyHint: ToBoolPtr(false), DestructiveHint: ToBoolPtr(true), }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description("Repository owner (username or organization)"), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("Repository name"), - ), + WithOwnerParam(), + WithRepoParam(), mcp.WithString("path", mcp.Required(), mcp.Description("Path to the file to delete"), @@ -748,7 +679,7 @@ func DeleteFile(getClient GetClientFn, t translations.TranslationHelperFunc) (to client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(ErrFailedToGetGitHubClient, err) } // Get the reference for the branch @@ -772,7 +703,7 @@ func DeleteFile(getClient GetClientFn, t translations.TranslationHelperFunc) (to if resp.StatusCode != http.StatusOK { body, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) + return nil, fmt.Errorf(ErrFailedToReadResponseBody, err) } return mcp.NewToolResultError(fmt.Sprintf("failed to get commit: %s", string(body))), nil } @@ -801,7 +732,7 @@ func DeleteFile(getClient GetClientFn, t translations.TranslationHelperFunc) (to if resp.StatusCode != http.StatusCreated { body, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) + return nil, fmt.Errorf(ErrFailedToReadResponseBody, err) } return mcp.NewToolResultError(fmt.Sprintf("failed to create tree: %s", string(body))), nil } @@ -825,7 +756,7 @@ func DeleteFile(getClient GetClientFn, t translations.TranslationHelperFunc) (to if resp.StatusCode != http.StatusCreated { body, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) + return nil, fmt.Errorf(ErrFailedToReadResponseBody, err) } return mcp.NewToolResultError(fmt.Sprintf("failed to create commit: %s", string(body))), nil } @@ -845,7 +776,7 @@ func DeleteFile(getClient GetClientFn, t translations.TranslationHelperFunc) (to if resp.StatusCode != http.StatusOK { body, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) + return nil, fmt.Errorf(ErrFailedToReadResponseBody, err) } return mcp.NewToolResultError(fmt.Sprintf("failed to update reference: %s", string(body))), nil } @@ -858,7 +789,7 @@ func DeleteFile(getClient GetClientFn, t translations.TranslationHelperFunc) (to r, err := json.Marshal(response) if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) + return nil, fmt.Errorf(ErrFailedToMarshalResponse, err) } return mcp.NewToolResultText(string(r)), nil @@ -873,14 +804,8 @@ func CreateBranch(getClient GetClientFn, t translations.TranslationHelperFunc) ( Title: t("TOOL_CREATE_BRANCH_USER_TITLE", "Create branch"), ReadOnlyHint: ToBoolPtr(false), }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description("Repository owner"), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("Repository name"), - ), + WithOwnerParam(), + WithRepoParam(), mcp.WithString("branch", mcp.Required(), mcp.Description("Name for new branch"), @@ -909,7 +834,7 @@ func CreateBranch(getClient GetClientFn, t translations.TranslationHelperFunc) ( client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(ErrFailedToGetGitHubClient, err) } // Get the source branch SHA @@ -959,7 +884,7 @@ func CreateBranch(getClient GetClientFn, t translations.TranslationHelperFunc) ( r, err := json.Marshal(createdRef) if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) + return nil, fmt.Errorf(ErrFailedToMarshalResponse, err) } return mcp.NewToolResultText(string(r)), nil @@ -974,14 +899,8 @@ func PushFiles(getClient GetClientFn, t translations.TranslationHelperFunc) (too Title: t("TOOL_PUSH_FILES_USER_TITLE", "Push files to repository"), ReadOnlyHint: ToBoolPtr(false), }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description("Repository owner"), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("Repository name"), - ), + WithOwnerParam(), + WithRepoParam(), mcp.WithString("branch", mcp.Required(), mcp.Description("Branch to push to"), @@ -1037,7 +956,7 @@ func PushFiles(getClient GetClientFn, t translations.TranslationHelperFunc) (too client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(ErrFailedToGetGitHubClient, err) } // Get the reference for the branch @@ -1131,7 +1050,7 @@ func PushFiles(getClient GetClientFn, t translations.TranslationHelperFunc) (too r, err := json.Marshal(updatedRef) if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) + return nil, fmt.Errorf(ErrFailedToMarshalResponse, err) } return mcp.NewToolResultText(string(r)), nil @@ -1146,14 +1065,8 @@ func ListTags(getClient GetClientFn, t translations.TranslationHelperFunc) (tool Title: t("TOOL_LIST_TAGS_USER_TITLE", "List tags"), ReadOnlyHint: ToBoolPtr(true), }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description("Repository owner"), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("Repository name"), - ), + WithOwnerParam(), + WithRepoParam(), WithPagination(), ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { @@ -1177,7 +1090,7 @@ func ListTags(getClient GetClientFn, t translations.TranslationHelperFunc) (tool client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(ErrFailedToGetGitHubClient, err) } tags, resp, err := client.Repositories.ListTags(ctx, owner, repo, opts) @@ -1193,14 +1106,14 @@ func ListTags(getClient GetClientFn, t translations.TranslationHelperFunc) (tool if resp.StatusCode != http.StatusOK { body, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) + return nil, fmt.Errorf(ErrFailedToReadResponseBody, err) } return mcp.NewToolResultError(fmt.Sprintf("failed to list tags: %s", string(body))), nil } r, err := json.Marshal(tags) if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) + return nil, fmt.Errorf(ErrFailedToMarshalResponse, err) } return mcp.NewToolResultText(string(r)), nil @@ -1215,14 +1128,8 @@ func GetTag(getClient GetClientFn, t translations.TranslationHelperFunc) (tool m Title: t("TOOL_GET_TAG_USER_TITLE", "Get tag details"), ReadOnlyHint: ToBoolPtr(true), }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description("Repository owner"), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("Repository name"), - ), + WithOwnerParam(), + WithRepoParam(), mcp.WithString("tag", mcp.Required(), mcp.Description("Tag name"), @@ -1244,7 +1151,7 @@ func GetTag(getClient GetClientFn, t translations.TranslationHelperFunc) (tool m client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(ErrFailedToGetGitHubClient, err) } // First get the tag reference @@ -1261,7 +1168,7 @@ func GetTag(getClient GetClientFn, t translations.TranslationHelperFunc) (tool m if resp.StatusCode != http.StatusOK { body, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) + return nil, fmt.Errorf(ErrFailedToReadResponseBody, err) } return mcp.NewToolResultError(fmt.Sprintf("failed to get tag reference: %s", string(body))), nil } @@ -1280,14 +1187,14 @@ func GetTag(getClient GetClientFn, t translations.TranslationHelperFunc) (tool m if resp.StatusCode != http.StatusOK { body, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) + return nil, fmt.Errorf(ErrFailedToReadResponseBody, err) } return mcp.NewToolResultError(fmt.Sprintf("failed to get tag object: %s", string(body))), nil } r, err := json.Marshal(tagObj) if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) + return nil, fmt.Errorf(ErrFailedToMarshalResponse, err) } return mcp.NewToolResultText(string(r)), nil diff --git a/pkg/github/repository_resource.go b/pkg/github/repository_resource.go index a454db630..394d6fcda 100644 --- a/pkg/github/repository_resource.go +++ b/pkg/github/repository_resource.go @@ -113,7 +113,7 @@ func RepositoryResourceContentsHandler(getClient GetClientFn, getRawClient raw.G // fetch the PR from the API to get the latest commit and use SHA githubClient, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(ErrFailedToGetGitHubClient, err) } prNum, err := strconv.Atoi(prNumber[0]) if err != nil { @@ -181,7 +181,7 @@ func RepositoryResourceContentsHandler(getClient GetClientFn, getRawClient raw.G // If we got a response but it is not 200 OK, we return an error body, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) + return nil, fmt.Errorf(ErrFailedToReadResponseBody, err) } return nil, fmt.Errorf("failed to fetch raw content: %s", string(body)) default: diff --git a/pkg/github/search.go b/pkg/github/search.go index 5106b84d8..5939c05e3 100644 --- a/pkg/github/search.go +++ b/pkg/github/search.go @@ -46,7 +46,7 @@ func SearchRepositories(getClient GetClientFn, t translations.TranslationHelperF client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(ErrFailedToGetGitHubClient, err) } result, resp, err := client.Search.Repositories(ctx, query, opts) if err != nil { @@ -61,17 +61,12 @@ func SearchRepositories(getClient GetClientFn, t translations.TranslationHelperF if resp.StatusCode != 200 { body, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) + return nil, fmt.Errorf(ErrFailedToReadResponseBody, err) } return mcp.NewToolResultError(fmt.Sprintf("failed to search repositories: %s", string(body))), nil } - r, err := json.Marshal(result) - if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) - } - - return mcp.NewToolResultText(string(r)), nil + return MarshalledTextResult(result), nil } } @@ -125,7 +120,7 @@ func SearchCode(getClient GetClientFn, t translations.TranslationHelperFunc) (to client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(ErrFailedToGetGitHubClient, err) } result, resp, err := client.Search.Code(ctx, query, opts) @@ -141,17 +136,12 @@ func SearchCode(getClient GetClientFn, t translations.TranslationHelperFunc) (to if resp.StatusCode != 200 { body, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) + return nil, fmt.Errorf(ErrFailedToReadResponseBody, err) } return mcp.NewToolResultError(fmt.Sprintf("failed to search code: %s", string(body))), nil } - r, err := json.Marshal(result) - if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) - } - - return mcp.NewToolResultText(string(r)), nil + return MarshalledTextResult(result), nil } } @@ -198,7 +188,7 @@ func userOrOrgHandler(accountType string, getClient GetClientFn) server.ToolHand client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return nil, fmt.Errorf(ErrFailedToGetGitHubClient, err) } searchQuery := "type:" + accountType + " " + query @@ -215,7 +205,7 @@ func userOrOrgHandler(accountType string, getClient GetClientFn) server.ToolHand if resp.StatusCode != 200 { body, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) + return nil, fmt.Errorf(ErrFailedToReadResponseBody, err) } return mcp.NewToolResultError(fmt.Sprintf("failed to search %ss: %s", accountType, string(body))), nil } @@ -251,7 +241,7 @@ func userOrOrgHandler(accountType string, getClient GetClientFn) server.ToolHand r, err := json.Marshal(minimalResp) if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) + return nil, fmt.Errorf(ErrFailedToMarshalResponse, err) } return mcp.NewToolResultText(string(r)), nil } diff --git a/pkg/github/secret_scanning.go b/pkg/github/secret_scanning.go index bea6df2ae..49d17c92d 100644 --- a/pkg/github/secret_scanning.go +++ b/pkg/github/secret_scanning.go @@ -2,12 +2,7 @@ package github import ( "context" - "encoding/json" - "fmt" - "io" - "net/http" - ghErrors "github.com/github/github-mcp-server/pkg/errors" "github.com/github/github-mcp-server/pkg/translations" "github.com/google/go-github/v72/github" "github.com/mark3labs/mcp-go/mcp" @@ -22,62 +17,25 @@ func GetSecretScanningAlert(getClient GetClientFn, t translations.TranslationHel Title: t("TOOL_GET_SECRET_SCANNING_ALERT_USER_TITLE", "Get secret scanning alert"), ReadOnlyHint: ToBoolPtr(true), }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description("The owner of the repository."), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("The name of the repository."), - ), - mcp.WithNumber("alertNumber", - mcp.Required(), - mcp.Description("The number of the alert."), - ), + WithOwnerParam(), + WithRepoParam(), + WithAlertNumberParam(), ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := RequiredParam[string](request, "owner") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - repo, err := RequiredParam[string](request, "repo") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - alertNumber, err := RequiredInt(request, "alertNumber") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - - client, err := getClient(ctx) - if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) - } - - alert, resp, err := client.SecretScanning.GetAlert(ctx, owner, repo, int64(alertNumber)) - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - fmt.Sprintf("failed to get alert with number '%d'", alertNumber), - resp, - err, - ), nil - } - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - return mcp.NewToolResultError(fmt.Sprintf("failed to get alert: %s", string(body))), nil - } - - r, err := json.Marshal(alert) - if err != nil { - return nil, fmt.Errorf("failed to marshal alert: %w", err) - } - - return mcp.NewToolResultText(string(r)), nil + return ExecuteWithClientAndValidation( + ctx, + getClient, + request, + func(req mcp.CallToolRequest) error { + _, _, _, err := ValidateOwnerRepoAlert(req) + return err + }, + func(ctx context.Context, client *github.Client) (*github.SecretScanningAlert, *github.Response, error) { + owner, repo, alertNumber, _ := ValidateOwnerRepoAlert(request) + return client.SecretScanning.GetAlert(ctx, owner, repo, int64(alertNumber)) + }, + "get alert", + ) } } @@ -89,14 +47,8 @@ func ListSecretScanningAlerts(getClient GetClientFn, t translations.TranslationH Title: t("TOOL_LIST_SECRET_SCANNING_ALERTS_USER_TITLE", "List secret scanning alerts"), ReadOnlyHint: ToBoolPtr(true), }), - mcp.WithString("owner", - mcp.Required(), - mcp.Description("The owner of the repository."), - ), - mcp.WithString("repo", - mcp.Required(), - mcp.Description("The name of the repository."), - ), + WithOwnerParam(), + WithRepoParam(), mcp.WithString("state", mcp.Description("Filter by state"), mcp.Enum("open", "resolved"), @@ -110,54 +62,22 @@ func ListSecretScanningAlerts(getClient GetClientFn, t translations.TranslationH ), ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - owner, err := RequiredParam[string](request, "owner") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - repo, err := RequiredParam[string](request, "repo") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - state, err := OptionalParam[string](request, "state") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - secretType, err := OptionalParam[string](request, "secret_type") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - resolution, err := OptionalParam[string](request, "resolution") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } - - client, err := getClient(ctx) - if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) - } - alerts, resp, err := client.SecretScanning.ListAlertsForRepo(ctx, owner, repo, &github.SecretScanningAlertListOptions{State: state, SecretType: secretType, Resolution: resolution}) - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, - fmt.Sprintf("failed to list alerts for repository '%s/%s'", owner, repo), - resp, - err, - ), nil - } - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode != http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - return mcp.NewToolResultError(fmt.Sprintf("failed to list alerts: %s", string(body))), nil - } - - r, err := json.Marshal(alerts) - if err != nil { - return nil, fmt.Errorf("failed to marshal alerts: %w", err) - } - - return mcp.NewToolResultText(string(r)), nil + return ExecuteWithClientAndValidation( + ctx, + getClient, + request, + func(req mcp.CallToolRequest) error { + _, _, err := ValidateOwnerRepo(req) + return err + }, + func(ctx context.Context, client *github.Client) ([]*github.SecretScanningAlert, *github.Response, error) { + owner, repo, _ := ValidateOwnerRepo(request) + state, _ := OptionalParam[string](request, "state") + secretType, _ := OptionalParam[string](request, "secret_type") + resolution, _ := OptionalParam[string](request, "resolution") + return client.SecretScanning.ListAlertsForRepo(ctx, owner, repo, &github.SecretScanningAlertListOptions{State: state, SecretType: secretType, Resolution: resolution}) + }, + "list alerts", + ) } } diff --git a/pkg/github/server.go b/pkg/github/server.go index 85d078f1b..ce1bd1487 100644 --- a/pkg/github/server.go +++ b/pkg/github/server.go @@ -1,15 +1,24 @@ package github import ( + "context" "encoding/json" "errors" "fmt" + "io" + ghErrors "github.com/github/github-mcp-server/pkg/errors" "github.com/google/go-github/v72/github" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" ) +const ( + ErrFailedToReadResponseBody = "failed to read response body: %w" + ErrFailedToMarshalResponse = "failed to marshal response: %w" + ErrFailedToGetGitHubClient = "failed to get GitHub client: %w" +) + // NewServer creates a new GitHub MCP server with the specified GH client and logger. func NewServer(version string, opts ...server.ServerOption) *server.MCPServer { @@ -224,3 +233,118 @@ func MarshalledTextResult(v any) *mcp.CallToolResult { return mcp.NewToolResultText(string(data)) } + +func HandleHTTPError(resp *github.Response, operation string) (*mcp.CallToolResult, error) { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + return mcp.NewToolResultError(fmt.Sprintf("failed to %s: %s", operation, string(body))), nil +} + +func ValidateOwnerRepoAlert(request mcp.CallToolRequest) (string, string, int, error) { + owner, err := RequiredParam[string](request, "owner") + if err != nil { + return "", "", 0, err + } + repo, err := RequiredParam[string](request, "repo") + if err != nil { + return "", "", 0, err + } + alertNumber, err := RequiredInt(request, "alertNumber") + if err != nil { + return "", "", 0, err + } + return owner, repo, alertNumber, nil +} + +func ValidateOwnerRepo(request mcp.CallToolRequest) (string, string, error) { + owner, err := RequiredParam[string](request, "owner") + if err != nil { + return "", "", err + } + repo, err := RequiredParam[string](request, "repo") + if err != nil { + return "", "", err + } + return owner, repo, nil +} + +func ExecuteWithClientAndValidation[T any]( + ctx context.Context, + getClient GetClientFn, + request mcp.CallToolRequest, + validateParams func(mcp.CallToolRequest) error, + apiCall func(context.Context, *github.Client) (T, *github.Response, error), + operation string, +) (*mcp.CallToolResult, error) { + if err := validateParams(request); err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf(ErrFailedToGetGitHubClient, err) + } + + result, resp, err := apiCall(ctx, client) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, fmt.Sprintf("failed to %s", operation), resp, err), nil + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != 200 { + return HandleHTTPError(resp, operation) + } + + return MarshalledTextResult(result), nil +} + +func WithOwnerParam() mcp.ToolOption { + return mcp.WithString("owner", + mcp.Required(), + mcp.Description("Repository owner"), + ) +} + +func WithRepoParam() mcp.ToolOption { + return mcp.WithString("repo", + mcp.Required(), + mcp.Description("Repository name"), + ) +} + +func WithPullNumberParam() mcp.ToolOption { + return mcp.WithNumber("pullNumber", + mcp.Required(), + mcp.Description("Pull request number"), + ) +} + +func WithIssueNumberParam() mcp.ToolOption { + return mcp.WithNumber("issue_number", + mcp.Required(), + mcp.Description("Issue number"), + ) +} + +func WithAlertNumberParam() mcp.ToolOption { + return mcp.WithNumber("alertNumber", + mcp.Required(), + mcp.Description("Alert number"), + ) +} + +func WithDiscussionNumberParam() mcp.ToolOption { + return mcp.WithNumber("discussionNumber", + mcp.Required(), + mcp.Description("Discussion number"), + ) +} + +func WithIssueNumberParamAlt() mcp.ToolOption { + return mcp.WithNumber("issueNumber", + mcp.Required(), + mcp.Description("Issue number"), + ) +}