diff --git a/README.md b/README.md index bcd9f85c8..e66da5aee 100644 --- a/README.md +++ b/README.md @@ -490,22 +490,22 @@ The following sets of tools are available: Actions -- **cancel_workflow_run** - Cancel workflow run +- **cancel_workflow_run** - Cancel workflow run (scopes: `repo`) - `owner`: Repository owner (string, required) - `repo`: Repository name (string, required) - `run_id`: The unique identifier of the workflow run (number, required) -- **delete_workflow_run_logs** - Delete workflow logs +- **delete_workflow_run_logs** - Delete workflow logs (scopes: `repo`) - `owner`: Repository owner (string, required) - `repo`: Repository name (string, required) - `run_id`: The unique identifier of the workflow run (number, required) -- **download_workflow_run_artifact** - Download workflow artifact +- **download_workflow_run_artifact** - Download workflow artifact (scopes: `repo`) - `artifact_id`: The unique identifier of the artifact (number, required) - `owner`: Repository owner (string, required) - `repo`: Repository name (string, required) -- **get_job_logs** - Get job logs +- **get_job_logs** - Get job logs (scopes: `repo`) - `failed_only`: When true, gets logs for all failed jobs in run_id (boolean, optional) - `job_id`: The unique identifier of the workflow job (required for single job logs) (number, optional) - `owner`: Repository owner (string, required) @@ -514,22 +514,22 @@ The following sets of tools are available: - `run_id`: Workflow run ID (required when using failed_only) (number, optional) - `tail_lines`: Number of lines to return from the end of the log (number, optional) -- **get_workflow_run** - Get workflow run +- **get_workflow_run** - Get workflow run (scopes: `repo`) - `owner`: Repository owner (string, required) - `repo`: Repository name (string, required) - `run_id`: The unique identifier of the workflow run (number, required) -- **get_workflow_run_logs** - Get workflow run logs +- **get_workflow_run_logs** - Get workflow run logs (scopes: `repo`) - `owner`: Repository owner (string, required) - `repo`: Repository name (string, required) - `run_id`: The unique identifier of the workflow run (number, required) -- **get_workflow_run_usage** - Get workflow usage +- **get_workflow_run_usage** - Get workflow usage (scopes: `repo`) - `owner`: Repository owner (string, required) - `repo`: Repository name (string, required) - `run_id`: The unique identifier of the workflow run (number, required) -- **list_workflow_jobs** - List workflow jobs +- **list_workflow_jobs** - List workflow jobs (scopes: `repo`) - `filter`: Filters jobs by their completed_at timestamp (string, optional) - `owner`: Repository owner (string, required) - `page`: Page number for pagination (min 1) (number, optional) @@ -537,14 +537,14 @@ The following sets of tools are available: - `repo`: Repository name (string, required) - `run_id`: The unique identifier of the workflow run (number, required) -- **list_workflow_run_artifacts** - List workflow artifacts +- **list_workflow_run_artifacts** - List workflow artifacts (scopes: `repo`) - `owner`: Repository owner (string, required) - `page`: Page number for pagination (min 1) (number, optional) - `perPage`: Results per page for pagination (min 1, max 100) (number, optional) - `repo`: Repository name (string, required) - `run_id`: The unique identifier of the workflow run (number, required) -- **list_workflow_runs** - List workflow runs +- **list_workflow_runs** - List workflow runs (scopes: `repo`) - `actor`: Returns someone's workflow runs. Use the login for the user who created the workflow run. (string, optional) - `branch`: Returns workflow runs associated with a branch. Use the name of the branch. (string, optional) - `event`: Returns workflow runs for a specific event type (string, optional) @@ -555,23 +555,23 @@ The following sets of tools are available: - `status`: Returns workflow runs with the check run status (string, optional) - `workflow_id`: The workflow ID or workflow file name (string, required) -- **list_workflows** - List workflows +- **list_workflows** - List workflows (scopes: `repo`) - `owner`: Repository owner (string, required) - `page`: Page number for pagination (min 1) (number, optional) - `perPage`: Results per page for pagination (min 1, max 100) (number, optional) - `repo`: Repository name (string, required) -- **rerun_failed_jobs** - Rerun failed jobs +- **rerun_failed_jobs** - Rerun failed jobs (scopes: `repo`) - `owner`: Repository owner (string, required) - `repo`: Repository name (string, required) - `run_id`: The unique identifier of the workflow run (number, required) -- **rerun_workflow_run** - Rerun workflow run +- **rerun_workflow_run** - Rerun workflow run (scopes: `repo`) - `owner`: Repository owner (string, required) - `repo`: Repository name (string, required) - `run_id`: The unique identifier of the workflow run (number, required) -- **run_workflow** - Run workflow +- **run_workflow** - Run workflow (scopes: `repo`) - `inputs`: Inputs the workflow accepts (object, optional) - `owner`: Repository owner (string, required) - `ref`: The git reference for the workflow. The reference can be a branch or tag name. (string, required) @@ -584,12 +584,12 @@ The following sets of tools are available: Code Security -- **get_code_scanning_alert** - Get code scanning alert +- **get_code_scanning_alert** - Get code scanning alert (scopes: `security_events`) - `alertNumber`: The number of the alert. (number, required) - `owner`: The owner of the repository. (string, required) - `repo`: The name of the repository. (string, required) -- **list_code_scanning_alerts** - List code scanning alerts +- **list_code_scanning_alerts** - List code scanning alerts (scopes: `security_events`) - `owner`: The owner of the repository. (string, required) - `ref`: The Git reference for the results you want to list. (string, optional) - `repo`: The name of the repository. (string, required) @@ -606,11 +606,11 @@ The following sets of tools are available: - **get_me** - Get my user profile - No parameters required -- **get_team_members** - Get team members +- **get_team_members** - Get team members (scopes: `read:org`) - `org`: Organization login (owner) that contains the team. (string, required) - `team_slug`: Team slug (string, required) -- **get_teams** - Get teams +- **get_teams** - Get teams (scopes: `read:org`) - `user`: Username to get teams for. If not provided, uses the authenticated user. (string, optional) @@ -619,12 +619,12 @@ The following sets of tools are available: Dependabot -- **get_dependabot_alert** - Get dependabot alert +- **get_dependabot_alert** - Get dependabot alert (scopes: `security_events`) - `alertNumber`: The number of the alert. (number, required) - `owner`: The owner of the repository. (string, required) - `repo`: The name of the repository. (string, required) -- **list_dependabot_alerts** - List dependabot alerts +- **list_dependabot_alerts** - List dependabot alerts (scopes: `security_events`) - `owner`: The owner of the repository. (string, required) - `repo`: The name of the repository. (string, required) - `severity`: Filter dependabot alerts by severity (string, optional) @@ -636,23 +636,23 @@ The following sets of tools are available: Discussions -- **get_discussion** - Get discussion +- **get_discussion** - Get discussion (scopes: `repo`) - `discussionNumber`: Discussion Number (number, required) - `owner`: Repository owner (string, required) - `repo`: Repository name (string, required) -- **get_discussion_comments** - Get discussion comments +- **get_discussion_comments** - Get discussion comments (scopes: `repo`) - `after`: Cursor for pagination. Use the endCursor from the previous page's PageInfo for GraphQL APIs. (string, optional) - `discussionNumber`: Discussion Number (number, required) - `owner`: Repository owner (string, required) - `perPage`: Results per page for pagination (min 1, max 100) (number, optional) - `repo`: Repository name (string, required) -- **list_discussion_categories** - List discussion categories +- **list_discussion_categories** - List discussion categories (scopes: `repo`) - `owner`: Repository owner (string, required) - `repo`: Repository name. If not provided, discussion categories will be queried at the organisation level. (string, optional) -- **list_discussions** - List discussions +- **list_discussions** - List discussions (scopes: `repo`) - `after`: Cursor for pagination. Use the endCursor from the previous page's PageInfo for GraphQL APIs. (string, optional) - `category`: Optional filter by discussion category ID. If provided, only discussions with this category are listed. (string, optional) - `direction`: Order direction. (string, optional) @@ -667,7 +667,7 @@ The following sets of tools are available: Gists -- **create_gist** - Create Gist +- **create_gist** - Create Gist (scopes: `gist`) - `content`: Content for simple single-file gist creation (string, required) - `description`: Description of the gist (string, optional) - `filename`: Filename for simple single-file gist creation (string, required) @@ -682,7 +682,7 @@ The following sets of tools are available: - `since`: Only gists updated after this time (ISO 8601 timestamp) (string, optional) - `username`: GitHub username (omit for authenticated user's gists) (string, optional) -- **update_gist** - Update Gist +- **update_gist** - Update Gist (scopes: `gist`) - `content`: Content for the file (string, required) - `description`: Updated description of the gist (string, optional) - `filename`: Filename to update or create (string, required) @@ -694,7 +694,7 @@ The following sets of tools are available: Git -- **get_repository_tree** - Get repository tree +- **get_repository_tree** - Get repository tree (scopes: `repo`) - `owner`: Repository owner (username or organization) (string, required) - `path_filter`: Optional path prefix to filter the tree results (e.g., 'src/' to only show files in the src directory) (string, optional) - `recursive`: Setting this parameter to true returns the objects or subtrees referenced by the tree. Default is false (boolean, optional) @@ -707,23 +707,23 @@ The following sets of tools are available: Issues -- **add_issue_comment** - Add comment to issue +- **add_issue_comment** - Add comment to issue (scopes: `repo`) - `body`: Comment content (string, required) - `issue_number`: Issue number to comment on (number, required) - `owner`: Repository owner (string, required) - `repo`: Repository name (string, required) -- **assign_copilot_to_issue** - Assign Copilot to issue +- **assign_copilot_to_issue** - Assign Copilot to issue (scopes: `repo`) - `issueNumber`: Issue number (number, required) - `owner`: Repository owner (string, required) - `repo`: Repository name (string, required) -- **get_label** - Get a specific label from a repository. +- **get_label** - Get a specific label from a repository. (scopes: `repo`) - `name`: Label name. (string, required) - `owner`: Repository owner (username or organization name) (string, required) - `repo`: Repository name (string, required) -- **issue_read** - Get issue details +- **issue_read** - Get issue details (scopes: `repo`) - `issue_number`: The number of the issue (number, required) - `method`: The read operation to perform on a single issue. Options are: @@ -737,7 +737,7 @@ The following sets of tools are available: - `perPage`: Results per page for pagination (min 1, max 100) (number, optional) - `repo`: The name of the repository (string, required) -- **issue_write** - Create or update issue. +- **issue_write** - Create or update issue. (scopes: `repo`) - `assignees`: Usernames to assign to this issue (string[], optional) - `body`: Issue body content (string, optional) - `duplicate_of`: Issue number that this issue is a duplicate of. Only used when state_reason is 'duplicate'. (number, optional) @@ -756,10 +756,10 @@ The following sets of tools are available: - `title`: Issue title (string, optional) - `type`: Type of this issue. Only use if the repository has issue types configured. Use list_issue_types tool to get valid type values for the organization. If the repository doesn't support issue types, omit this parameter. (string, optional) -- **list_issue_types** - List available issue types +- **list_issue_types** - List available issue types (scopes: `read:org`) - `owner`: The organization owner of the repository (string, required) -- **list_issues** - List issues +- **list_issues** - List issues (scopes: `repo`) - `after`: Cursor for pagination. Use the endCursor from the previous page's PageInfo for GraphQL APIs. (string, optional) - `direction`: Order direction. If provided, the 'orderBy' also needs to be provided. (string, optional) - `labels`: Filter by labels (string[], optional) @@ -770,7 +770,7 @@ The following sets of tools are available: - `since`: Filter by date (ISO 8601 timestamp) (string, optional) - `state`: Filter by state, by default both open and closed issues are returned when not provided (string, optional) -- **search_issues** - Search issues +- **search_issues** - Search issues (scopes: `repo`) - `order`: Sort order (string, optional) - `owner`: Optional repository owner. If provided with repo, only issues for this repository are listed. (string, optional) - `page`: Page number for pagination (min 1) (number, optional) @@ -779,7 +779,7 @@ The following sets of tools are available: - `repo`: Optional repository name. If provided with owner, only issues for this repository are listed. (string, optional) - `sort`: Sort field by number of matches of categories, defaults to best match (string, optional) -- **sub_issue_write** - Change sub-issue +- **sub_issue_write** - Change sub-issue (scopes: `repo`) - `after_id`: The ID of the sub-issue to be prioritized after (either after_id OR before_id should be specified) (number, optional) - `before_id`: The ID of the sub-issue to be prioritized before (either after_id OR before_id should be specified) (number, optional) - `issue_number`: The number of the parent issue (number, required) @@ -800,12 +800,12 @@ The following sets of tools are available: Labels -- **get_label** - Get a specific label from a repository. +- **get_label** - Get a specific label from a repository. (scopes: `repo`) - `name`: Label name. (string, required) - `owner`: Repository owner (username or organization name) (string, required) - `repo`: Repository name (string, required) -- **label_write** - Write operations on repository labels. +- **label_write** - Write operations on repository labels. (scopes: `repo`) - `color`: Label color as 6-character hex code without '#' prefix (e.g., 'f29513'). Required for 'create', optional for 'update'. (string, optional) - `description`: Label description text. Optional for 'create' and 'update'. (string, optional) - `method`: Operation to perform: 'create', 'update', or 'delete' (string, required) @@ -814,7 +814,7 @@ The following sets of tools are available: - `owner`: Repository owner (username or organization name) (string, required) - `repo`: Repository name (string, required) -- **list_label** - List labels from a repository +- **list_label** - List labels from a repository (scopes: `repo`) - `owner`: Repository owner (username or organization name) - required for all operations (string, required) - `repo`: Repository name - required for all operations (string, required) @@ -824,14 +824,14 @@ The following sets of tools are available: Notifications -- **dismiss_notification** - Dismiss notification +- **dismiss_notification** - Dismiss notification (scopes: `notifications`) - `state`: The new state of the notification (read/done) (string, required) - `threadID`: The ID of the notification thread (string, required) -- **get_notification_details** - Get notification details +- **get_notification_details** - Get notification details (scopes: `notifications`) - `notificationID`: The ID of the notification (string, required) -- **list_notifications** - List notifications +- **list_notifications** - List notifications (scopes: `notifications`) - `before`: Only show notifications updated before the given time (ISO 8601 format) (string, optional) - `filter`: Filter notifications to, use default unless specified. Read notifications are ones that have already been acknowledged by the user. Participating notifications are those that the user is directly involved in, such as issues or pull requests they have commented on or created. (string, optional) - `owner`: Optional repository owner. If provided with repo, only notifications for this repository are listed. (string, optional) @@ -840,16 +840,16 @@ The following sets of tools are available: - `repo`: Optional repository name. If provided with owner, only notifications for this repository are listed. (string, optional) - `since`: Only show notifications updated after the given time (ISO 8601 format) (string, optional) -- **manage_notification_subscription** - Manage notification subscription +- **manage_notification_subscription** - Manage notification subscription (scopes: `notifications`) - `action`: Action to perform: ignore, watch, or delete the notification subscription. (string, required) - `notificationID`: The ID of the notification thread. (string, required) -- **manage_repository_notification_subscription** - Manage repository notification subscription +- **manage_repository_notification_subscription** - Manage repository notification subscription (scopes: `notifications`) - `action`: Action to perform: ignore, watch, or delete the repository notification subscription. (string, required) - `owner`: The account owner of the repository. (string, required) - `repo`: The name of the repository. (string, required) -- **mark_all_notifications_read** - Mark all notifications as read +- **mark_all_notifications_read** - Mark all notifications as read (scopes: `notifications`) - `lastReadAt`: Describes the last point that notifications were checked (optional). Default: Now (string, optional) - `owner`: Optional repository owner. If provided with repo, only notifications for this repository are marked as read. (string, optional) - `repo`: Optional repository name. If provided with owner, only notifications for this repository are marked as read. (string, optional) @@ -860,7 +860,7 @@ The following sets of tools are available: Organizations -- **search_orgs** - Search organizations +- **search_orgs** - Search organizations (scopes: `repo`) - `order`: Sort order (string, optional) - `page`: Page number for pagination (min 1) (number, optional) - `perPage`: Results per page for pagination (min 1, max 100) (number, optional) @@ -873,38 +873,38 @@ The following sets of tools are available: Projects -- **add_project_item** - Add project item +- **add_project_item** - Add project item (scopes: `project`) - `item_id`: The numeric ID of the issue or pull request to add to the project. (number, required) - `item_type`: The item's type, either issue or pull_request. (string, required) - `owner`: If owner_type == user it is the handle for the GitHub user account. If owner_type == org it is the name of the organization. The name is not case sensitive. (string, required) - `owner_type`: Owner type (string, required) - `project_number`: The project's number. (number, required) -- **delete_project_item** - Delete project item +- **delete_project_item** - Delete project item (scopes: `project`) - `item_id`: The internal project item ID to delete from the project (not the issue or pull request ID). (number, required) - `owner`: If owner_type == user it is the handle for the GitHub user account. If owner_type == org it is the name of the organization. The name is not case sensitive. (string, required) - `owner_type`: Owner type (string, required) - `project_number`: The project's number. (number, required) -- **get_project** - Get project +- **get_project** - Get project (scopes: `read:project`) - `owner`: If owner_type == user it is the handle for the GitHub user account. If owner_type == org it is the name of the organization. The name is not case sensitive. (string, required) - `owner_type`: Owner type (string, required) - `project_number`: The project's number (number, required) -- **get_project_field** - Get project field +- **get_project_field** - Get project field (scopes: `read:project`) - `field_id`: The field's id. (number, required) - `owner`: If owner_type == user it is the handle for the GitHub user account. If owner_type == org it is the name of the organization. The name is not case sensitive. (string, required) - `owner_type`: Owner type (string, required) - `project_number`: The project's number. (number, required) -- **get_project_item** - Get project item +- **get_project_item** - Get project item (scopes: `read:project`) - `fields`: Specific list of field IDs to include in the response (e.g. ["102589", "985201", "169875"]). If not provided, only the title field is included. (string[], optional) - `item_id`: The item's ID. (number, required) - `owner`: If owner_type == user it is the handle for the GitHub user account. If owner_type == org it is the name of the organization. The name is not case sensitive. (string, required) - `owner_type`: Owner type (string, required) - `project_number`: The project's number. (number, required) -- **list_project_fields** - List project fields +- **list_project_fields** - List project fields (scopes: `read:project`) - `after`: Forward pagination cursor from previous pageInfo.nextCursor. (string, optional) - `before`: Backward pagination cursor from previous pageInfo.prevCursor (rare). (string, optional) - `owner`: If owner_type == user it is the handle for the GitHub user account. If owner_type == org it is the name of the organization. The name is not case sensitive. (string, required) @@ -912,7 +912,7 @@ The following sets of tools are available: - `per_page`: Results per page (max 50) (number, optional) - `project_number`: The project's number. (number, required) -- **list_project_items** - List project items +- **list_project_items** - List project items (scopes: `read:project`) - `after`: Forward pagination cursor from previous pageInfo.nextCursor. (string, optional) - `before`: Backward pagination cursor from previous pageInfo.prevCursor (rare). (string, optional) - `fields`: Field IDs to include (e.g. ["102589", "985201"]). CRITICAL: Always provide to get field values. Without this, only titles returned. (string[], optional) @@ -922,7 +922,7 @@ The following sets of tools are available: - `project_number`: The project's number. (number, required) - `query`: Query string for advanced filtering of project items using GitHub's project filtering syntax. (string, optional) -- **list_projects** - List projects +- **list_projects** - List projects (scopes: `read:project`) - `after`: Forward pagination cursor from previous pageInfo.nextCursor. (string, optional) - `before`: Backward pagination cursor from previous pageInfo.prevCursor (rare). (string, optional) - `owner`: If owner_type == user it is the handle for the GitHub user account. If owner_type == org it is the name of the organization. The name is not case sensitive. (string, required) @@ -930,7 +930,7 @@ The following sets of tools are available: - `per_page`: Results per page (max 50) (number, optional) - `query`: Filter projects by title text and open/closed state; permitted qualifiers: is:open, is:closed; examples: "roadmap is:open", "is:open feature planning". (string, optional) -- **update_project_item** - Update project item +- **update_project_item** - Update project item (scopes: `project`) - `item_id`: The unique identifier of the project item. This is not the issue or pull request ID. (number, required) - `owner`: If owner_type == user it is the handle for the GitHub user account. If owner_type == org it is the name of the organization. The name is not case sensitive. (string, required) - `owner_type`: Owner type (string, required) @@ -943,7 +943,7 @@ The following sets of tools are available: Pull Requests -- **add_comment_to_pending_review** - Add review comment to the requester's latest pending pull request review +- **add_comment_to_pending_review** - Add review comment to the requester's latest pending pull request review (scopes: `repo`) - `body`: The text of the review comment (string, required) - `line`: The line of the blob in the pull request diff that the comment applies to. For multi-line comments, the last line of the range (number, optional) - `owner`: Repository owner (string, required) @@ -955,7 +955,7 @@ The following sets of tools are available: - `startSide`: For multi-line comments, the starting side of the diff that the comment applies to. LEFT indicates the previous state, RIGHT indicates the new state (string, optional) - `subjectType`: The level at which the comment is targeted (string, required) -- **create_pull_request** - Open new pull request +- **create_pull_request** - Open new pull request (scopes: `repo`) - `base`: Branch to merge into (string, required) - `body`: PR description (string, optional) - `draft`: Create as draft PR (boolean, optional) @@ -965,7 +965,7 @@ The following sets of tools are available: - `repo`: Repository name (string, required) - `title`: PR title (string, required) -- **list_pull_requests** - List pull requests +- **list_pull_requests** - List pull requests (scopes: `repo`) - `base`: Filter by base branch (string, optional) - `direction`: Sort direction (string, optional) - `head`: Filter by head user/org and branch (string, optional) @@ -976,7 +976,7 @@ The following sets of tools are available: - `sort`: Sort by (string, optional) - `state`: Filter by state (string, optional) -- **merge_pull_request** - Merge pull request +- **merge_pull_request** - Merge pull request (scopes: `repo`) - `commit_message`: Extra detail for merge commit (string, optional) - `commit_title`: Title for merge commit (string, optional) - `merge_method`: Merge method (string, optional) @@ -984,7 +984,7 @@ The following sets of tools are available: - `pullNumber`: Pull request number (number, required) - `repo`: Repository name (string, required) -- **pull_request_read** - Get details for a single pull request +- **pull_request_read** - Get details for a single pull request (scopes: `repo`) - `method`: Action to specify what pull request data needs to be retrieved from GitHub. Possible options: 1. get - Get details of a specific pull request. @@ -1001,7 +1001,7 @@ The following sets of tools are available: - `pullNumber`: Pull request number (number, required) - `repo`: Repository name (string, required) -- **pull_request_review_write** - Write operations (create, submit, delete) on pull request reviews. +- **pull_request_review_write** - Write operations (create, submit, delete) on pull request reviews. (scopes: `repo`) - `body`: Review comment text (string, optional) - `commitID`: SHA of commit to review (string, optional) - `event`: Review action to perform. (string, optional) @@ -1010,12 +1010,12 @@ The following sets of tools are available: - `pullNumber`: Pull request number (number, required) - `repo`: Repository name (string, required) -- **request_copilot_review** - Request Copilot review +- **request_copilot_review** - Request Copilot review (scopes: `repo`) - `owner`: Repository owner (string, required) - `pullNumber`: Pull request number (number, required) - `repo`: Repository name (string, required) -- **search_pull_requests** - Search pull requests +- **search_pull_requests** - Search pull requests (scopes: `repo`) - `order`: Sort order (string, optional) - `owner`: Optional repository owner. If provided with repo, only pull requests for this repository are listed. (string, optional) - `page`: Page number for pagination (min 1) (number, optional) @@ -1024,7 +1024,7 @@ The following sets of tools are available: - `repo`: Optional repository name. If provided with owner, only pull requests for this repository are listed. (string, optional) - `sort`: Sort field by number of matches of categories, defaults to best match (string, optional) -- **update_pull_request** - Edit pull request +- **update_pull_request** - Edit pull request (scopes: `repo`) - `base`: New base branch name (string, optional) - `body`: New description (string, optional) - `draft`: Mark pull request as draft (true) or ready for review (false) (boolean, optional) @@ -1036,7 +1036,7 @@ The following sets of tools are available: - `state`: New state (string, optional) - `title`: New title (string, optional) -- **update_pull_request_branch** - Update pull request branch +- **update_pull_request_branch** - Update pull request branch (scopes: `repo`) - `expectedHeadSha`: The expected SHA of the pull request's HEAD ref (string, optional) - `owner`: Repository owner (string, required) - `pullNumber`: Pull request number (number, required) @@ -1048,13 +1048,13 @@ The following sets of tools are available: Repositories -- **create_branch** - Create branch +- **create_branch** - Create branch (scopes: `repo`) - `branch`: Name for new branch (string, required) - `from_branch`: Source branch (defaults to repo default) (string, optional) - `owner`: Repository owner (string, required) - `repo`: Repository name (string, required) -- **create_or_update_file** - Create or update file +- **create_or_update_file** - Create or update file (scopes: `repo`) - `branch`: Branch to create/update the file in (string, required) - `content`: Content of the file (string, required) - `message`: Commit message (string, required) @@ -1063,26 +1063,26 @@ The following sets of tools are available: - `repo`: Repository name (string, required) - `sha`: Required if updating an existing file. The blob SHA of the file being replaced. (string, optional) -- **create_repository** - Create repository +- **create_repository** - Create repository (scopes: `repo`) - `autoInit`: Initialize with README (boolean, optional) - `description`: Repository description (string, optional) - `name`: Repository name (string, required) - `organization`: Organization to create the repository in (omit to create in your personal account) (string, optional) - `private`: Whether repo should be private (boolean, optional) -- **delete_file** - Delete file +- **delete_file** - Delete file (scopes: `repo`) - `branch`: Branch to delete the file from (string, required) - `message`: Commit message (string, required) - `owner`: Repository owner (username or organization) (string, required) - `path`: Path to the file to delete (string, required) - `repo`: Repository name (string, required) -- **fork_repository** - Fork repository +- **fork_repository** - Fork repository (scopes: `repo`) - `organization`: Organization to fork to (string, optional) - `owner`: Repository owner (string, required) - `repo`: Repository name (string, required) -- **get_commit** - Get commit details +- **get_commit** - Get commit details (scopes: `repo`) - `include_diff`: Whether to include file diffs and stats in the response. Default is true. (boolean, optional) - `owner`: Repository owner (string, required) - `page`: Page number for pagination (min 1) (number, optional) @@ -1090,34 +1090,34 @@ The following sets of tools are available: - `repo`: Repository name (string, required) - `sha`: Commit SHA, branch name, or tag name (string, required) -- **get_file_contents** - Get file or directory contents +- **get_file_contents** - Get file or directory contents (scopes: `repo`) - `owner`: Repository owner (username or organization) (string, required) - `path`: Path to file/directory (directories must end with a slash '/') (string, optional) - `ref`: Accepts optional git refs such as `refs/tags/{tag}`, `refs/heads/{branch}` or `refs/pull/{pr_number}/head` (string, optional) - `repo`: Repository name (string, required) - `sha`: Accepts optional commit SHA. If specified, it will be used instead of ref (string, optional) -- **get_latest_release** - Get latest release +- **get_latest_release** - Get latest release (scopes: `repo`) - `owner`: Repository owner (string, required) - `repo`: Repository name (string, required) -- **get_release_by_tag** - Get a release by tag name +- **get_release_by_tag** - Get a release by tag name (scopes: `repo`) - `owner`: Repository owner (string, required) - `repo`: Repository name (string, required) - `tag`: Tag name (e.g., 'v1.0.0') (string, required) -- **get_tag** - Get tag details +- **get_tag** - Get tag details (scopes: `repo`) - `owner`: Repository owner (string, required) - `repo`: Repository name (string, required) - `tag`: Tag name (string, required) -- **list_branches** - List branches +- **list_branches** - List branches (scopes: `repo`) - `owner`: Repository owner (string, required) - `page`: Page number for pagination (min 1) (number, optional) - `perPage`: Results per page for pagination (min 1, max 100) (number, optional) - `repo`: Repository name (string, required) -- **list_commits** - List commits +- **list_commits** - List commits (scopes: `repo`) - `author`: Author username or email address to filter commits by (string, optional) - `owner`: Repository owner (string, required) - `page`: Page number for pagination (min 1) (number, optional) @@ -1125,33 +1125,33 @@ The following sets of tools are available: - `repo`: Repository name (string, required) - `sha`: Commit SHA, branch or tag name to list commits of. If not provided, uses the default branch of the repository. If a commit SHA is provided, will list commits up to that SHA. (string, optional) -- **list_releases** - List releases +- **list_releases** - List releases (scopes: `repo`) - `owner`: Repository owner (string, required) - `page`: Page number for pagination (min 1) (number, optional) - `perPage`: Results per page for pagination (min 1, max 100) (number, optional) - `repo`: Repository name (string, required) -- **list_tags** - List tags +- **list_tags** - List tags (scopes: `repo`) - `owner`: Repository owner (string, required) - `page`: Page number for pagination (min 1) (number, optional) - `perPage`: Results per page for pagination (min 1, max 100) (number, optional) - `repo`: Repository name (string, required) -- **push_files** - Push files to repository +- **push_files** - Push files to repository (scopes: `repo`) - `branch`: Branch to push to (string, required) - `files`: Array of file objects to push, each object with path (string) and content (string) (object[], required) - `message`: Commit message (string, required) - `owner`: Repository owner (string, required) - `repo`: Repository name (string, required) -- **search_code** - Search code +- **search_code** - Search code (scopes: `repo`) - `order`: Sort order for results (string, optional) - `page`: Page number for pagination (min 1) (number, optional) - `perPage`: Results per page for pagination (min 1, max 100) (number, optional) - `query`: Search query using GitHub's powerful code search syntax. Examples: 'content:Skill language:Java org:github', 'NOT is:archived language:Python OR language:go', 'repo:github/github-mcp-server'. Supports exact matching, language filters, path filters, and more. (string, required) - `sort`: Sort field ('indexed' only) (string, optional) -- **search_repositories** - Search repositories +- **search_repositories** - Search repositories (scopes: `repo`) - `minimal_output`: Return minimal repository information (default: true). When false, returns full GitHub API repository objects. (boolean, optional) - `order`: Sort order (string, optional) - `page`: Page number for pagination (min 1) (number, optional) @@ -1165,12 +1165,12 @@ The following sets of tools are available: Secret Protection -- **get_secret_scanning_alert** - Get secret scanning alert +- **get_secret_scanning_alert** - Get secret scanning alert (scopes: `security_events`) - `alertNumber`: The number of the alert. (number, required) - `owner`: The owner of the repository. (string, required) - `repo`: The name of the repository. (string, required) -- **list_secret_scanning_alerts** - List secret scanning alerts +- **list_secret_scanning_alerts** - List secret scanning alerts (scopes: `security_events`) - `owner`: The owner of the repository. (string, required) - `repo`: The name of the repository. (string, required) - `resolution`: Filter by resolution (string, optional) @@ -1183,10 +1183,10 @@ The following sets of tools are available: Security Advisories -- **get_global_security_advisory** - Get a global security advisory +- **get_global_security_advisory** - Get a global security advisory (scopes: `security_events`) - `ghsaId`: GitHub Security Advisory ID (format: GHSA-xxxx-xxxx-xxxx). (string, required) -- **list_global_security_advisories** - List global security advisories +- **list_global_security_advisories** - List global security advisories (scopes: `security_events`) - `affects`: Filter advisories by affected package or version (e.g. "package1,package2@1.0.0"). (string, optional) - `cveId`: Filter by CVE ID. (string, optional) - `cwes`: Filter by Common Weakness Enumeration IDs (e.g. ["79", "284", "22"]). (string[], optional) @@ -1199,13 +1199,13 @@ The following sets of tools are available: - `type`: Advisory type. (string, optional) - `updated`: Filter by update date or date range (ISO 8601 date or range). (string, optional) -- **list_org_repository_security_advisories** - List org repository security advisories +- **list_org_repository_security_advisories** - List org repository security advisories (scopes: `security_events`) - `direction`: Sort direction. (string, optional) - `org`: The organization login. (string, required) - `sort`: Sort field. (string, optional) - `state`: Filter by advisory state. (string, optional) -- **list_repository_security_advisories** - List repository security advisories +- **list_repository_security_advisories** - List repository security advisories (scopes: `security_events`) - `direction`: Sort direction. (string, optional) - `owner`: The owner of the repository. (string, required) - `repo`: The name of the repository. (string, required) @@ -1225,11 +1225,11 @@ The following sets of tools are available: - `sort`: How to sort the results. Can be either 'created' (when the repository was starred) or 'updated' (when the repository was last pushed to). (string, optional) - `username`: Username to list starred repositories for. Defaults to the authenticated user. (string, optional) -- **star_repository** - Star repository +- **star_repository** - Star repository (scopes: `public_repo`) - `owner`: Repository owner (string, required) - `repo`: Repository name (string, required) -- **unstar_repository** - Unstar repository +- **unstar_repository** - Unstar repository (scopes: `public_repo`) - `owner`: Repository owner (string, required) - `repo`: Repository name (string, required) @@ -1239,7 +1239,7 @@ The following sets of tools are available: Users -- **search_users** - Search users +- **search_users** - Search users (scopes: `repo`) - `order`: Sort order (string, optional) - `page`: Page number for pagination (min 1) (number, optional) - `perPage`: Results per page for pagination (min 1, max 100) (number, optional) diff --git a/cmd/github-mcp-server/generate_docs.go b/cmd/github-mcp-server/generate_docs.go index 61459d7f0..84ede9993 100644 --- a/cmd/github-mcp-server/generate_docs.go +++ b/cmd/github-mcp-server/generate_docs.go @@ -12,6 +12,7 @@ import ( "github.com/github/github-mcp-server/pkg/github" "github.com/github/github-mcp-server/pkg/lockdown" "github.com/github/github-mcp-server/pkg/raw" + "github.com/github/github-mcp-server/pkg/scopes" "github.com/github/github-mcp-server/pkg/toolsets" "github.com/github/github-mcp-server/pkg/translations" gogithub "github.com/google/go-github/v79/github" @@ -224,7 +225,22 @@ func generateToolDoc(tool mcp.Tool) string { var lines []string // Tool name only (using annotation name instead of verbose description) - lines = append(lines, fmt.Sprintf("- **%s** - %s", tool.Name, tool.Annotations.Title)) + toolLine := fmt.Sprintf("- **%s** - %s", tool.Name, tool.Annotations.Title) + + // Add required scopes if present (skip NoScope which is empty string) + if tool.Meta != nil { + toolScopes := scopes.GetScopesFromMeta(tool.Meta) + var scopeStrs []string + for _, s := range toolScopes { + if s != scopes.NoScope { + scopeStrs = append(scopeStrs, fmt.Sprintf("`%s`", s.String())) + } + } + if len(scopeStrs) > 0 { + toolLine += fmt.Sprintf(" (scopes: %s)", strings.Join(scopeStrs, ", ")) + } + } + lines = append(lines, toolLine) // Parameters if tool.InputSchema == nil { diff --git a/pkg/github/__toolsnaps__/add_comment_to_pending_review.snap b/pkg/github/__toolsnaps__/add_comment_to_pending_review.snap index 78795c096..3ef08fbe8 100644 --- a/pkg/github/__toolsnaps__/add_comment_to_pending_review.snap +++ b/pkg/github/__toolsnaps__/add_comment_to_pending_review.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "pull_requests" + }, "annotations": { "title": "Add review comment to the requester's latest pending pull request review" }, diff --git a/pkg/github/__toolsnaps__/add_issue_comment.snap b/pkg/github/__toolsnaps__/add_issue_comment.snap index fb2a9e7b3..153c9f656 100644 --- a/pkg/github/__toolsnaps__/add_issue_comment.snap +++ b/pkg/github/__toolsnaps__/add_issue_comment.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "issues" + }, "annotations": { "title": "Add comment to issue" }, diff --git a/pkg/github/__toolsnaps__/add_project_item.snap b/pkg/github/__toolsnaps__/add_project_item.snap index 08f495370..8bcfb4913 100644 --- a/pkg/github/__toolsnaps__/add_project_item.snap +++ b/pkg/github/__toolsnaps__/add_project_item.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "project" + ], + "toolset": "projects" + }, "annotations": { "title": "Add project item" }, diff --git a/pkg/github/__toolsnaps__/assign_copilot_to_issue.snap b/pkg/github/__toolsnaps__/assign_copilot_to_issue.snap index e250ca9c1..3f01ee7a4 100644 --- a/pkg/github/__toolsnaps__/assign_copilot_to_issue.snap +++ b/pkg/github/__toolsnaps__/assign_copilot_to_issue.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "issues" + }, "annotations": { "idempotentHint": true, "title": "Assign Copilot to issue" diff --git a/pkg/github/__toolsnaps__/cancel_workflow_run.snap b/pkg/github/__toolsnaps__/cancel_workflow_run.snap index 83eb31a7f..6f84aa19d 100644 --- a/pkg/github/__toolsnaps__/cancel_workflow_run.snap +++ b/pkg/github/__toolsnaps__/cancel_workflow_run.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "actions" + }, "annotations": { "title": "Cancel workflow run" }, diff --git a/pkg/github/__toolsnaps__/create_branch.snap b/pkg/github/__toolsnaps__/create_branch.snap index 675a2de9c..86240c898 100644 --- a/pkg/github/__toolsnaps__/create_branch.snap +++ b/pkg/github/__toolsnaps__/create_branch.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "repos" + }, "annotations": { "title": "Create branch" }, diff --git a/pkg/github/__toolsnaps__/create_gist.snap b/pkg/github/__toolsnaps__/create_gist.snap index 465206ab4..8b4299bb9 100644 --- a/pkg/github/__toolsnaps__/create_gist.snap +++ b/pkg/github/__toolsnaps__/create_gist.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "gist" + ], + "toolset": "gists" + }, "annotations": { "title": "Create Gist" }, diff --git a/pkg/github/__toolsnaps__/create_or_update_file.snap b/pkg/github/__toolsnaps__/create_or_update_file.snap index 4ec2ae914..df4bd1050 100644 --- a/pkg/github/__toolsnaps__/create_or_update_file.snap +++ b/pkg/github/__toolsnaps__/create_or_update_file.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "repos" + }, "annotations": { "title": "Create or update file" }, diff --git a/pkg/github/__toolsnaps__/create_pull_request.snap b/pkg/github/__toolsnaps__/create_pull_request.snap index 80f0b9863..2cc52d203 100644 --- a/pkg/github/__toolsnaps__/create_pull_request.snap +++ b/pkg/github/__toolsnaps__/create_pull_request.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "pull_requests" + }, "annotations": { "title": "Open new pull request" }, diff --git a/pkg/github/__toolsnaps__/create_repository.snap b/pkg/github/__toolsnaps__/create_repository.snap index 290767c66..584defd56 100644 --- a/pkg/github/__toolsnaps__/create_repository.snap +++ b/pkg/github/__toolsnaps__/create_repository.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "repos" + }, "annotations": { "title": "Create repository" }, diff --git a/pkg/github/__toolsnaps__/delete_file.snap b/pkg/github/__toolsnaps__/delete_file.snap index b985154e8..bfab820e3 100644 --- a/pkg/github/__toolsnaps__/delete_file.snap +++ b/pkg/github/__toolsnaps__/delete_file.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "repos" + }, "annotations": { "destructiveHint": true, "title": "Delete file" diff --git a/pkg/github/__toolsnaps__/delete_project_item.snap b/pkg/github/__toolsnaps__/delete_project_item.snap index d768df10f..f409958b0 100644 --- a/pkg/github/__toolsnaps__/delete_project_item.snap +++ b/pkg/github/__toolsnaps__/delete_project_item.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "project" + ], + "toolset": "projects" + }, "annotations": { "title": "Delete project item" }, diff --git a/pkg/github/__toolsnaps__/delete_workflow_run_logs.snap b/pkg/github/__toolsnaps__/delete_workflow_run_logs.snap index fc9a5cd46..8af2e5f46 100644 --- a/pkg/github/__toolsnaps__/delete_workflow_run_logs.snap +++ b/pkg/github/__toolsnaps__/delete_workflow_run_logs.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "actions" + }, "annotations": { "destructiveHint": true, "title": "Delete workflow logs" diff --git a/pkg/github/__toolsnaps__/dismiss_notification.snap b/pkg/github/__toolsnaps__/dismiss_notification.snap index b0125ba53..96e31669b 100644 --- a/pkg/github/__toolsnaps__/dismiss_notification.snap +++ b/pkg/github/__toolsnaps__/dismiss_notification.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "notifications" + ], + "toolset": "notifications" + }, "annotations": { "title": "Dismiss notification" }, diff --git a/pkg/github/__toolsnaps__/download_workflow_run_artifact.snap b/pkg/github/__toolsnaps__/download_workflow_run_artifact.snap index c4d89872c..e6c302001 100644 --- a/pkg/github/__toolsnaps__/download_workflow_run_artifact.snap +++ b/pkg/github/__toolsnaps__/download_workflow_run_artifact.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "actions" + }, "annotations": { "readOnlyHint": true, "title": "Download workflow artifact" diff --git a/pkg/github/__toolsnaps__/fork_repository.snap b/pkg/github/__toolsnaps__/fork_repository.snap index c195bd7d2..2b8b296d7 100644 --- a/pkg/github/__toolsnaps__/fork_repository.snap +++ b/pkg/github/__toolsnaps__/fork_repository.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "repos" + }, "annotations": { "title": "Fork repository" }, diff --git a/pkg/github/__toolsnaps__/get_code_scanning_alert.snap b/pkg/github/__toolsnaps__/get_code_scanning_alert.snap index 9e46b960a..a59da7da4 100644 --- a/pkg/github/__toolsnaps__/get_code_scanning_alert.snap +++ b/pkg/github/__toolsnaps__/get_code_scanning_alert.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "security_events" + ], + "toolset": "code_security" + }, "annotations": { "readOnlyHint": true, "title": "Get code scanning alert" diff --git a/pkg/github/__toolsnaps__/get_commit.snap b/pkg/github/__toolsnaps__/get_commit.snap index c6b96d5ed..3c735ad96 100644 --- a/pkg/github/__toolsnaps__/get_commit.snap +++ b/pkg/github/__toolsnaps__/get_commit.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "repos" + }, "annotations": { "readOnlyHint": true, "title": "Get commit details" diff --git a/pkg/github/__toolsnaps__/get_dependabot_alert.snap b/pkg/github/__toolsnaps__/get_dependabot_alert.snap index a517809e2..f4e222830 100644 --- a/pkg/github/__toolsnaps__/get_dependabot_alert.snap +++ b/pkg/github/__toolsnaps__/get_dependabot_alert.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "security_events" + ], + "toolset": "dependabot" + }, "annotations": { "readOnlyHint": true, "title": "Get dependabot alert" diff --git a/pkg/github/__toolsnaps__/get_discussion.snap b/pkg/github/__toolsnaps__/get_discussion.snap index feef0f057..fd520b55a 100644 --- a/pkg/github/__toolsnaps__/get_discussion.snap +++ b/pkg/github/__toolsnaps__/get_discussion.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "discussions" + }, "annotations": { "readOnlyHint": true, "title": "Get discussion" diff --git a/pkg/github/__toolsnaps__/get_discussion_comments.snap b/pkg/github/__toolsnaps__/get_discussion_comments.snap index 3af5edc8c..51e726319 100644 --- a/pkg/github/__toolsnaps__/get_discussion_comments.snap +++ b/pkg/github/__toolsnaps__/get_discussion_comments.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "discussions" + }, "annotations": { "readOnlyHint": true, "title": "Get discussion comments" diff --git a/pkg/github/__toolsnaps__/get_file_contents.snap b/pkg/github/__toolsnaps__/get_file_contents.snap index 767466dd3..03d9ec1c7 100644 --- a/pkg/github/__toolsnaps__/get_file_contents.snap +++ b/pkg/github/__toolsnaps__/get_file_contents.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "repos" + }, "annotations": { "readOnlyHint": true, "title": "Get file or directory contents" diff --git a/pkg/github/__toolsnaps__/get_gist.snap b/pkg/github/__toolsnaps__/get_gist.snap index 4d2661822..5cea46643 100644 --- a/pkg/github/__toolsnaps__/get_gist.snap +++ b/pkg/github/__toolsnaps__/get_gist.snap @@ -1,4 +1,7 @@ { + "_meta": { + "toolset": "gists" + }, "annotations": { "readOnlyHint": true, "title": "Get Gist Content" diff --git a/pkg/github/__toolsnaps__/get_global_security_advisory.snap b/pkg/github/__toolsnaps__/get_global_security_advisory.snap index 18c30425a..ed827894c 100644 --- a/pkg/github/__toolsnaps__/get_global_security_advisory.snap +++ b/pkg/github/__toolsnaps__/get_global_security_advisory.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "security_events" + ], + "toolset": "security_advisories" + }, "annotations": { "readOnlyHint": true, "title": "Get a global security advisory" diff --git a/pkg/github/__toolsnaps__/get_job_logs.snap b/pkg/github/__toolsnaps__/get_job_logs.snap index 8b2319527..d55a97bff 100644 --- a/pkg/github/__toolsnaps__/get_job_logs.snap +++ b/pkg/github/__toolsnaps__/get_job_logs.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "actions" + }, "annotations": { "readOnlyHint": true, "title": "Get job logs" diff --git a/pkg/github/__toolsnaps__/get_label.snap b/pkg/github/__toolsnaps__/get_label.snap index 8541044d0..a8ab8d3e8 100644 --- a/pkg/github/__toolsnaps__/get_label.snap +++ b/pkg/github/__toolsnaps__/get_label.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "labels" + }, "annotations": { "readOnlyHint": true, "title": "Get a specific label from a repository." diff --git a/pkg/github/__toolsnaps__/get_latest_release.snap b/pkg/github/__toolsnaps__/get_latest_release.snap index 23b551a0f..09f613a73 100644 --- a/pkg/github/__toolsnaps__/get_latest_release.snap +++ b/pkg/github/__toolsnaps__/get_latest_release.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "repos" + }, "annotations": { "readOnlyHint": true, "title": "Get latest release" diff --git a/pkg/github/__toolsnaps__/get_me.snap b/pkg/github/__toolsnaps__/get_me.snap index e6d02929f..2fcb2e7ac 100644 --- a/pkg/github/__toolsnaps__/get_me.snap +++ b/pkg/github/__toolsnaps__/get_me.snap @@ -1,4 +1,7 @@ { + "_meta": { + "toolset": "context" + }, "annotations": { "readOnlyHint": true, "title": "Get my user profile" diff --git a/pkg/github/__toolsnaps__/get_notification_details.snap b/pkg/github/__toolsnaps__/get_notification_details.snap index de197f2b1..733ac5a7e 100644 --- a/pkg/github/__toolsnaps__/get_notification_details.snap +++ b/pkg/github/__toolsnaps__/get_notification_details.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "notifications" + ], + "toolset": "notifications" + }, "annotations": { "readOnlyHint": true, "title": "Get notification details" diff --git a/pkg/github/__toolsnaps__/get_project.snap b/pkg/github/__toolsnaps__/get_project.snap index 8194b7358..fd649b2df 100644 --- a/pkg/github/__toolsnaps__/get_project.snap +++ b/pkg/github/__toolsnaps__/get_project.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "read:project" + ], + "toolset": "projects" + }, "annotations": { "readOnlyHint": true, "title": "Get project" diff --git a/pkg/github/__toolsnaps__/get_project_field.snap b/pkg/github/__toolsnaps__/get_project_field.snap index 0df557a03..73461a593 100644 --- a/pkg/github/__toolsnaps__/get_project_field.snap +++ b/pkg/github/__toolsnaps__/get_project_field.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "read:project" + ], + "toolset": "projects" + }, "annotations": { "readOnlyHint": true, "title": "Get project field" diff --git a/pkg/github/__toolsnaps__/get_project_item.snap b/pkg/github/__toolsnaps__/get_project_item.snap index d77c49c1e..b59774221 100644 --- a/pkg/github/__toolsnaps__/get_project_item.snap +++ b/pkg/github/__toolsnaps__/get_project_item.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "read:project" + ], + "toolset": "projects" + }, "annotations": { "readOnlyHint": true, "title": "Get project item" diff --git a/pkg/github/__toolsnaps__/get_release_by_tag.snap b/pkg/github/__toolsnaps__/get_release_by_tag.snap index 77f19488c..bed5d4bcb 100644 --- a/pkg/github/__toolsnaps__/get_release_by_tag.snap +++ b/pkg/github/__toolsnaps__/get_release_by_tag.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "repos" + }, "annotations": { "readOnlyHint": true, "title": "Get a release by tag name" diff --git a/pkg/github/__toolsnaps__/get_repository_tree.snap b/pkg/github/__toolsnaps__/get_repository_tree.snap index 882462883..e4d8aed12 100644 --- a/pkg/github/__toolsnaps__/get_repository_tree.snap +++ b/pkg/github/__toolsnaps__/get_repository_tree.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "git" + }, "annotations": { "readOnlyHint": true, "title": "Get repository tree" diff --git a/pkg/github/__toolsnaps__/get_secret_scanning_alert.snap b/pkg/github/__toolsnaps__/get_secret_scanning_alert.snap index 4d55011da..93ba9bc40 100644 --- a/pkg/github/__toolsnaps__/get_secret_scanning_alert.snap +++ b/pkg/github/__toolsnaps__/get_secret_scanning_alert.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "security_events" + ], + "toolset": "secret_protection" + }, "annotations": { "readOnlyHint": true, "title": "Get secret scanning alert" diff --git a/pkg/github/__toolsnaps__/get_tag.snap b/pkg/github/__toolsnaps__/get_tag.snap index e33f5c2e4..4b7783672 100644 --- a/pkg/github/__toolsnaps__/get_tag.snap +++ b/pkg/github/__toolsnaps__/get_tag.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "repos" + }, "annotations": { "readOnlyHint": true, "title": "Get tag details" diff --git a/pkg/github/__toolsnaps__/get_team_members.snap b/pkg/github/__toolsnaps__/get_team_members.snap index 5b7f090fe..018a2bbfd 100644 --- a/pkg/github/__toolsnaps__/get_team_members.snap +++ b/pkg/github/__toolsnaps__/get_team_members.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "read:org" + ], + "toolset": "context" + }, "annotations": { "readOnlyHint": true, "title": "Get team members" diff --git a/pkg/github/__toolsnaps__/get_teams.snap b/pkg/github/__toolsnaps__/get_teams.snap index 595dd262d..b7165fe79 100644 --- a/pkg/github/__toolsnaps__/get_teams.snap +++ b/pkg/github/__toolsnaps__/get_teams.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "read:org" + ], + "toolset": "context" + }, "annotations": { "readOnlyHint": true, "title": "Get teams" diff --git a/pkg/github/__toolsnaps__/get_workflow_run.snap b/pkg/github/__toolsnaps__/get_workflow_run.snap index 37921ffad..3db3bea3a 100644 --- a/pkg/github/__toolsnaps__/get_workflow_run.snap +++ b/pkg/github/__toolsnaps__/get_workflow_run.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "actions" + }, "annotations": { "readOnlyHint": true, "title": "Get workflow run" diff --git a/pkg/github/__toolsnaps__/get_workflow_run_logs.snap b/pkg/github/__toolsnaps__/get_workflow_run_logs.snap index 77fb619b7..ac3cceed6 100644 --- a/pkg/github/__toolsnaps__/get_workflow_run_logs.snap +++ b/pkg/github/__toolsnaps__/get_workflow_run_logs.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "actions" + }, "annotations": { "readOnlyHint": true, "title": "Get workflow run logs" diff --git a/pkg/github/__toolsnaps__/get_workflow_run_usage.snap b/pkg/github/__toolsnaps__/get_workflow_run_usage.snap index c9fe49f96..6ae2666d1 100644 --- a/pkg/github/__toolsnaps__/get_workflow_run_usage.snap +++ b/pkg/github/__toolsnaps__/get_workflow_run_usage.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "actions" + }, "annotations": { "readOnlyHint": true, "title": "Get workflow usage" diff --git a/pkg/github/__toolsnaps__/issue_read.snap b/pkg/github/__toolsnaps__/issue_read.snap index c6a9e7306..f84cd43d5 100644 --- a/pkg/github/__toolsnaps__/issue_read.snap +++ b/pkg/github/__toolsnaps__/issue_read.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "issues" + }, "annotations": { "readOnlyHint": true, "title": "Get issue details" diff --git a/pkg/github/__toolsnaps__/issue_write.snap b/pkg/github/__toolsnaps__/issue_write.snap index 8c6634a02..53ba37b5d 100644 --- a/pkg/github/__toolsnaps__/issue_write.snap +++ b/pkg/github/__toolsnaps__/issue_write.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "issues" + }, "annotations": { "title": "Create or update issue." }, diff --git a/pkg/github/__toolsnaps__/label_write.snap b/pkg/github/__toolsnaps__/label_write.snap index 879817442..48233d898 100644 --- a/pkg/github/__toolsnaps__/label_write.snap +++ b/pkg/github/__toolsnaps__/label_write.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "labels" + }, "annotations": { "title": "Write operations on repository labels." }, diff --git a/pkg/github/__toolsnaps__/list_branches.snap b/pkg/github/__toolsnaps__/list_branches.snap index b589c9b7e..007f89273 100644 --- a/pkg/github/__toolsnaps__/list_branches.snap +++ b/pkg/github/__toolsnaps__/list_branches.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "repos" + }, "annotations": { "readOnlyHint": true, "title": "List branches" diff --git a/pkg/github/__toolsnaps__/list_code_scanning_alerts.snap b/pkg/github/__toolsnaps__/list_code_scanning_alerts.snap index 6f2a4e342..e302c60b6 100644 --- a/pkg/github/__toolsnaps__/list_code_scanning_alerts.snap +++ b/pkg/github/__toolsnaps__/list_code_scanning_alerts.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "security_events" + ], + "toolset": "code_security" + }, "annotations": { "readOnlyHint": true, "title": "List code scanning alerts" diff --git a/pkg/github/__toolsnaps__/list_commits.snap b/pkg/github/__toolsnaps__/list_commits.snap index bd67602ed..a264aa0d8 100644 --- a/pkg/github/__toolsnaps__/list_commits.snap +++ b/pkg/github/__toolsnaps__/list_commits.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "repos" + }, "annotations": { "readOnlyHint": true, "title": "List commits" diff --git a/pkg/github/__toolsnaps__/list_dependabot_alerts.snap b/pkg/github/__toolsnaps__/list_dependabot_alerts.snap index d96d3972c..5b7ae464e 100644 --- a/pkg/github/__toolsnaps__/list_dependabot_alerts.snap +++ b/pkg/github/__toolsnaps__/list_dependabot_alerts.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "security_events" + ], + "toolset": "dependabot" + }, "annotations": { "readOnlyHint": true, "title": "List dependabot alerts" diff --git a/pkg/github/__toolsnaps__/list_discussion_categories.snap b/pkg/github/__toolsnaps__/list_discussion_categories.snap index 888ebbdca..63c4b1705 100644 --- a/pkg/github/__toolsnaps__/list_discussion_categories.snap +++ b/pkg/github/__toolsnaps__/list_discussion_categories.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "discussions" + }, "annotations": { "readOnlyHint": true, "title": "List discussion categories" diff --git a/pkg/github/__toolsnaps__/list_discussions.snap b/pkg/github/__toolsnaps__/list_discussions.snap index 95a8bebf5..bdb2becd0 100644 --- a/pkg/github/__toolsnaps__/list_discussions.snap +++ b/pkg/github/__toolsnaps__/list_discussions.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "discussions" + }, "annotations": { "readOnlyHint": true, "title": "List discussions" diff --git a/pkg/github/__toolsnaps__/list_gists.snap b/pkg/github/__toolsnaps__/list_gists.snap index 834b45205..e6ae6e689 100644 --- a/pkg/github/__toolsnaps__/list_gists.snap +++ b/pkg/github/__toolsnaps__/list_gists.snap @@ -1,4 +1,7 @@ { + "_meta": { + "toolset": "gists" + }, "annotations": { "readOnlyHint": true, "title": "List Gists" diff --git a/pkg/github/__toolsnaps__/list_global_security_advisories.snap b/pkg/github/__toolsnaps__/list_global_security_advisories.snap index fd9fa78c5..0cbd5055c 100644 --- a/pkg/github/__toolsnaps__/list_global_security_advisories.snap +++ b/pkg/github/__toolsnaps__/list_global_security_advisories.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "security_events" + ], + "toolset": "security_advisories" + }, "annotations": { "readOnlyHint": true, "title": "List global security advisories" diff --git a/pkg/github/__toolsnaps__/list_issue_types.snap b/pkg/github/__toolsnaps__/list_issue_types.snap index b17dcc54f..bba4d3357 100644 --- a/pkg/github/__toolsnaps__/list_issue_types.snap +++ b/pkg/github/__toolsnaps__/list_issue_types.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "read:org" + ], + "toolset": "issues" + }, "annotations": { "readOnlyHint": true, "title": "List available issue types" diff --git a/pkg/github/__toolsnaps__/list_issues.snap b/pkg/github/__toolsnaps__/list_issues.snap index 9d6b55586..8b433ac26 100644 --- a/pkg/github/__toolsnaps__/list_issues.snap +++ b/pkg/github/__toolsnaps__/list_issues.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "issues" + }, "annotations": { "readOnlyHint": true, "title": "List issues" diff --git a/pkg/github/__toolsnaps__/list_label.snap b/pkg/github/__toolsnaps__/list_label.snap index 0b4f3b20c..65899ec3e 100644 --- a/pkg/github/__toolsnaps__/list_label.snap +++ b/pkg/github/__toolsnaps__/list_label.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "labels" + }, "annotations": { "readOnlyHint": true, "title": "List labels from a repository." diff --git a/pkg/github/__toolsnaps__/list_notifications.snap b/pkg/github/__toolsnaps__/list_notifications.snap index ae43e0f25..f78549592 100644 --- a/pkg/github/__toolsnaps__/list_notifications.snap +++ b/pkg/github/__toolsnaps__/list_notifications.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "notifications" + ], + "toolset": "notifications" + }, "annotations": { "readOnlyHint": true, "title": "List notifications" diff --git a/pkg/github/__toolsnaps__/list_org_repository_security_advisories.snap b/pkg/github/__toolsnaps__/list_org_repository_security_advisories.snap index 5f8823659..581cf4fa2 100644 --- a/pkg/github/__toolsnaps__/list_org_repository_security_advisories.snap +++ b/pkg/github/__toolsnaps__/list_org_repository_security_advisories.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "security_events" + ], + "toolset": "security_advisories" + }, "annotations": { "readOnlyHint": true, "title": "List org repository security advisories" diff --git a/pkg/github/__toolsnaps__/list_project_fields.snap b/pkg/github/__toolsnaps__/list_project_fields.snap index 6bef18507..ea87d6188 100644 --- a/pkg/github/__toolsnaps__/list_project_fields.snap +++ b/pkg/github/__toolsnaps__/list_project_fields.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "read:project" + ], + "toolset": "projects" + }, "annotations": { "readOnlyHint": true, "title": "List project fields" diff --git a/pkg/github/__toolsnaps__/list_project_items.snap b/pkg/github/__toolsnaps__/list_project_items.snap index bceb5d9eb..ce0d1ad0b 100644 --- a/pkg/github/__toolsnaps__/list_project_items.snap +++ b/pkg/github/__toolsnaps__/list_project_items.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "read:project" + ], + "toolset": "projects" + }, "annotations": { "readOnlyHint": true, "title": "List project items" diff --git a/pkg/github/__toolsnaps__/list_projects.snap b/pkg/github/__toolsnaps__/list_projects.snap index f48e26217..2cf6fbcf5 100644 --- a/pkg/github/__toolsnaps__/list_projects.snap +++ b/pkg/github/__toolsnaps__/list_projects.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "read:project" + ], + "toolset": "projects" + }, "annotations": { "readOnlyHint": true, "title": "List projects" diff --git a/pkg/github/__toolsnaps__/list_pull_requests.snap b/pkg/github/__toolsnaps__/list_pull_requests.snap index ae90c3fe0..4d76cdf7b 100644 --- a/pkg/github/__toolsnaps__/list_pull_requests.snap +++ b/pkg/github/__toolsnaps__/list_pull_requests.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "pull_requests" + }, "annotations": { "readOnlyHint": true, "title": "List pull requests" diff --git a/pkg/github/__toolsnaps__/list_releases.snap b/pkg/github/__toolsnaps__/list_releases.snap index 98d4ce66f..25602054b 100644 --- a/pkg/github/__toolsnaps__/list_releases.snap +++ b/pkg/github/__toolsnaps__/list_releases.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "repos" + }, "annotations": { "readOnlyHint": true, "title": "List releases" diff --git a/pkg/github/__toolsnaps__/list_repository_security_advisories.snap b/pkg/github/__toolsnaps__/list_repository_security_advisories.snap index 465fd881e..0fc75275d 100644 --- a/pkg/github/__toolsnaps__/list_repository_security_advisories.snap +++ b/pkg/github/__toolsnaps__/list_repository_security_advisories.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "security_events" + ], + "toolset": "security_advisories" + }, "annotations": { "readOnlyHint": true, "title": "List repository security advisories" diff --git a/pkg/github/__toolsnaps__/list_secret_scanning_alerts.snap b/pkg/github/__toolsnaps__/list_secret_scanning_alerts.snap index e7896c55f..f30073d7f 100644 --- a/pkg/github/__toolsnaps__/list_secret_scanning_alerts.snap +++ b/pkg/github/__toolsnaps__/list_secret_scanning_alerts.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "security_events" + ], + "toolset": "secret_protection" + }, "annotations": { "readOnlyHint": true, "title": "List secret scanning alerts" diff --git a/pkg/github/__toolsnaps__/list_tags.snap b/pkg/github/__toolsnaps__/list_tags.snap index 5b667d19c..0d286544f 100644 --- a/pkg/github/__toolsnaps__/list_tags.snap +++ b/pkg/github/__toolsnaps__/list_tags.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "repos" + }, "annotations": { "readOnlyHint": true, "title": "List tags" diff --git a/pkg/github/__toolsnaps__/list_workflow_jobs.snap b/pkg/github/__toolsnaps__/list_workflow_jobs.snap index 59ff75afc..aace7cbb8 100644 --- a/pkg/github/__toolsnaps__/list_workflow_jobs.snap +++ b/pkg/github/__toolsnaps__/list_workflow_jobs.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "actions" + }, "annotations": { "readOnlyHint": true, "title": "List workflow jobs" diff --git a/pkg/github/__toolsnaps__/list_workflow_run_artifacts.snap b/pkg/github/__toolsnaps__/list_workflow_run_artifacts.snap index 6d6332d74..6f0912155 100644 --- a/pkg/github/__toolsnaps__/list_workflow_run_artifacts.snap +++ b/pkg/github/__toolsnaps__/list_workflow_run_artifacts.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "actions" + }, "annotations": { "readOnlyHint": true, "title": "List workflow artifacts" diff --git a/pkg/github/__toolsnaps__/list_workflow_runs.snap b/pkg/github/__toolsnaps__/list_workflow_runs.snap index e5353f490..036494e84 100644 --- a/pkg/github/__toolsnaps__/list_workflow_runs.snap +++ b/pkg/github/__toolsnaps__/list_workflow_runs.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "actions" + }, "annotations": { "readOnlyHint": true, "title": "List workflow runs" diff --git a/pkg/github/__toolsnaps__/list_workflows.snap b/pkg/github/__toolsnaps__/list_workflows.snap index f3f52f042..091e00db1 100644 --- a/pkg/github/__toolsnaps__/list_workflows.snap +++ b/pkg/github/__toolsnaps__/list_workflows.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "actions" + }, "annotations": { "readOnlyHint": true, "title": "List workflows" diff --git a/pkg/github/__toolsnaps__/manage_notification_subscription.snap b/pkg/github/__toolsnaps__/manage_notification_subscription.snap index 4f0d466a0..2606c343b 100644 --- a/pkg/github/__toolsnaps__/manage_notification_subscription.snap +++ b/pkg/github/__toolsnaps__/manage_notification_subscription.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "notifications" + ], + "toolset": "notifications" + }, "annotations": { "title": "Manage notification subscription" }, diff --git a/pkg/github/__toolsnaps__/manage_repository_notification_subscription.snap b/pkg/github/__toolsnaps__/manage_repository_notification_subscription.snap index 82ee40a89..867be7c2d 100644 --- a/pkg/github/__toolsnaps__/manage_repository_notification_subscription.snap +++ b/pkg/github/__toolsnaps__/manage_repository_notification_subscription.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "notifications" + ], + "toolset": "notifications" + }, "annotations": { "title": "Manage repository notification subscription" }, diff --git a/pkg/github/__toolsnaps__/mark_all_notifications_read.snap b/pkg/github/__toolsnaps__/mark_all_notifications_read.snap index 2d45ed78d..7ff03b624 100644 --- a/pkg/github/__toolsnaps__/mark_all_notifications_read.snap +++ b/pkg/github/__toolsnaps__/mark_all_notifications_read.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "notifications" + ], + "toolset": "notifications" + }, "annotations": { "title": "Mark all notifications as read" }, diff --git a/pkg/github/__toolsnaps__/merge_pull_request.snap b/pkg/github/__toolsnaps__/merge_pull_request.snap index 50d040f2a..38d46c4e1 100644 --- a/pkg/github/__toolsnaps__/merge_pull_request.snap +++ b/pkg/github/__toolsnaps__/merge_pull_request.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "pull_requests" + }, "annotations": { "title": "Merge pull request" }, diff --git a/pkg/github/__toolsnaps__/pull_request_read.snap b/pkg/github/__toolsnaps__/pull_request_read.snap index 434fba348..6b23b4c59 100644 --- a/pkg/github/__toolsnaps__/pull_request_read.snap +++ b/pkg/github/__toolsnaps__/pull_request_read.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "pull_requests" + }, "annotations": { "readOnlyHint": true, "title": "Get details for a single pull request" diff --git a/pkg/github/__toolsnaps__/pull_request_review_write.snap b/pkg/github/__toolsnaps__/pull_request_review_write.snap index 92cc19924..70959b104 100644 --- a/pkg/github/__toolsnaps__/pull_request_review_write.snap +++ b/pkg/github/__toolsnaps__/pull_request_review_write.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "pull_requests" + }, "annotations": { "title": "Write operations (create, submit, delete) on pull request reviews." }, diff --git a/pkg/github/__toolsnaps__/push_files.snap b/pkg/github/__toolsnaps__/push_files.snap index 4db764cc9..f3f03425f 100644 --- a/pkg/github/__toolsnaps__/push_files.snap +++ b/pkg/github/__toolsnaps__/push_files.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "repos" + }, "annotations": { "title": "Push files to repository" }, diff --git a/pkg/github/__toolsnaps__/request_copilot_review.snap b/pkg/github/__toolsnaps__/request_copilot_review.snap index b967b51cc..c6d33188e 100644 --- a/pkg/github/__toolsnaps__/request_copilot_review.snap +++ b/pkg/github/__toolsnaps__/request_copilot_review.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "pull_requests" + }, "annotations": { "title": "Request Copilot review" }, diff --git a/pkg/github/__toolsnaps__/rerun_failed_jobs.snap b/pkg/github/__toolsnaps__/rerun_failed_jobs.snap index 2c627637c..655922e7d 100644 --- a/pkg/github/__toolsnaps__/rerun_failed_jobs.snap +++ b/pkg/github/__toolsnaps__/rerun_failed_jobs.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "actions" + }, "annotations": { "title": "Rerun failed jobs" }, diff --git a/pkg/github/__toolsnaps__/rerun_workflow_run.snap b/pkg/github/__toolsnaps__/rerun_workflow_run.snap index 00514ee79..1978771f1 100644 --- a/pkg/github/__toolsnaps__/rerun_workflow_run.snap +++ b/pkg/github/__toolsnaps__/rerun_workflow_run.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "actions" + }, "annotations": { "title": "Rerun workflow run" }, diff --git a/pkg/github/__toolsnaps__/run_workflow.snap b/pkg/github/__toolsnaps__/run_workflow.snap index bb35e8213..497fa92d4 100644 --- a/pkg/github/__toolsnaps__/run_workflow.snap +++ b/pkg/github/__toolsnaps__/run_workflow.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "actions" + }, "annotations": { "title": "Run workflow" }, diff --git a/pkg/github/__toolsnaps__/search_code.snap b/pkg/github/__toolsnaps__/search_code.snap index aebd432bf..a1529f252 100644 --- a/pkg/github/__toolsnaps__/search_code.snap +++ b/pkg/github/__toolsnaps__/search_code.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "repos" + }, "annotations": { "readOnlyHint": true, "title": "Search code" diff --git a/pkg/github/__toolsnaps__/search_issues.snap b/pkg/github/__toolsnaps__/search_issues.snap index f76a715fb..f7988305b 100644 --- a/pkg/github/__toolsnaps__/search_issues.snap +++ b/pkg/github/__toolsnaps__/search_issues.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "issues" + }, "annotations": { "readOnlyHint": true, "title": "Search issues" diff --git a/pkg/github/__toolsnaps__/search_orgs.snap b/pkg/github/__toolsnaps__/search_orgs.snap index 36eb948ae..62d45c147 100644 --- a/pkg/github/__toolsnaps__/search_orgs.snap +++ b/pkg/github/__toolsnaps__/search_orgs.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "orgs" + }, "annotations": { "readOnlyHint": true, "title": "Search organizations" diff --git a/pkg/github/__toolsnaps__/search_pull_requests.snap b/pkg/github/__toolsnaps__/search_pull_requests.snap index 2013f5c08..35a5b64bc 100644 --- a/pkg/github/__toolsnaps__/search_pull_requests.snap +++ b/pkg/github/__toolsnaps__/search_pull_requests.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "pull_requests" + }, "annotations": { "readOnlyHint": true, "title": "Search pull requests" diff --git a/pkg/github/__toolsnaps__/search_repositories.snap b/pkg/github/__toolsnaps__/search_repositories.snap index 881bc3816..147005195 100644 --- a/pkg/github/__toolsnaps__/search_repositories.snap +++ b/pkg/github/__toolsnaps__/search_repositories.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "repos" + }, "annotations": { "readOnlyHint": true, "title": "Search repositories" diff --git a/pkg/github/__toolsnaps__/search_users.snap b/pkg/github/__toolsnaps__/search_users.snap index 293107696..597d76684 100644 --- a/pkg/github/__toolsnaps__/search_users.snap +++ b/pkg/github/__toolsnaps__/search_users.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "users" + }, "annotations": { "readOnlyHint": true, "title": "Search users" diff --git a/pkg/github/__toolsnaps__/star_repository.snap b/pkg/github/__toolsnaps__/star_repository.snap index 382d40395..dd245a5d5 100644 --- a/pkg/github/__toolsnaps__/star_repository.snap +++ b/pkg/github/__toolsnaps__/star_repository.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "public_repo" + ], + "toolset": "stargazers" + }, "annotations": { "title": "Star repository" }, diff --git a/pkg/github/__toolsnaps__/sub_issue_write.snap b/pkg/github/__toolsnaps__/sub_issue_write.snap index 1c721a2bb..37c94a173 100644 --- a/pkg/github/__toolsnaps__/sub_issue_write.snap +++ b/pkg/github/__toolsnaps__/sub_issue_write.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "issues" + }, "annotations": { "title": "Change sub-issue" }, diff --git a/pkg/github/__toolsnaps__/unstar_repository.snap b/pkg/github/__toolsnaps__/unstar_repository.snap index 709453650..d17378eb4 100644 --- a/pkg/github/__toolsnaps__/unstar_repository.snap +++ b/pkg/github/__toolsnaps__/unstar_repository.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "public_repo" + ], + "toolset": "stargazers" + }, "annotations": { "title": "Unstar repository" }, diff --git a/pkg/github/__toolsnaps__/update_gist.snap b/pkg/github/__toolsnaps__/update_gist.snap index a3907a88c..740770059 100644 --- a/pkg/github/__toolsnaps__/update_gist.snap +++ b/pkg/github/__toolsnaps__/update_gist.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "gist" + ], + "toolset": "gists" + }, "annotations": { "title": "Update Gist" }, diff --git a/pkg/github/__toolsnaps__/update_project_item.snap b/pkg/github/__toolsnaps__/update_project_item.snap index 8f5afaa58..4fe2a5a43 100644 --- a/pkg/github/__toolsnaps__/update_project_item.snap +++ b/pkg/github/__toolsnaps__/update_project_item.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "project" + ], + "toolset": "projects" + }, "annotations": { "title": "Update project item" }, diff --git a/pkg/github/__toolsnaps__/update_pull_request.snap b/pkg/github/__toolsnaps__/update_pull_request.snap index 6dec2c01f..1adf8cf70 100644 --- a/pkg/github/__toolsnaps__/update_pull_request.snap +++ b/pkg/github/__toolsnaps__/update_pull_request.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "pull_requests" + }, "annotations": { "title": "Edit pull request" }, diff --git a/pkg/github/__toolsnaps__/update_pull_request_branch.snap b/pkg/github/__toolsnaps__/update_pull_request_branch.snap index 9be1cb002..b660f3000 100644 --- a/pkg/github/__toolsnaps__/update_pull_request_branch.snap +++ b/pkg/github/__toolsnaps__/update_pull_request_branch.snap @@ -1,4 +1,10 @@ { + "_meta": { + "requiredOAuthScopes": [ + "repo" + ], + "toolset": "pull_requests" + }, "annotations": { "title": "Update pull request branch" }, diff --git a/pkg/github/actions.go b/pkg/github/actions.go index 81ed55296..a46648e82 100644 --- a/pkg/github/actions.go +++ b/pkg/github/actions.go @@ -11,6 +11,7 @@ import ( "github.com/github/github-mcp-server/internal/profiler" buffer "github.com/github/github-mcp-server/pkg/buffer" ghErrors "github.com/github/github-mcp-server/pkg/errors" + "github.com/github/github-mcp-server/pkg/scopes" "github.com/github/github-mcp-server/pkg/translations" "github.com/github/github-mcp-server/pkg/utils" "github.com/google/go-github/v79/github" @@ -28,6 +29,7 @@ func ListWorkflows(getClient GetClientFn, t translations.TranslationHelperFunc) return mcp.Tool{ Name: "list_workflows", Description: t("TOOL_LIST_WORKFLOWS_DESCRIPTION", "List workflows in a repository"), + Meta: NewToolMeta(ToolsetMetadataActions, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_LIST_WORKFLOWS_USER_TITLE", "List workflows"), ReadOnlyHint: true, @@ -94,6 +96,7 @@ func ListWorkflowRuns(getClient GetClientFn, t translations.TranslationHelperFun return mcp.Tool{ Name: "list_workflow_runs", Description: t("TOOL_LIST_WORKFLOW_RUNS_DESCRIPTION", "List workflow runs for a specific workflow"), + Meta: NewToolMeta(ToolsetMetadataActions, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_LIST_WORKFLOW_RUNS_USER_TITLE", "List workflow runs"), ReadOnlyHint: true, @@ -243,6 +246,7 @@ func RunWorkflow(getClient GetClientFn, t translations.TranslationHelperFunc) (m return mcp.Tool{ Name: "run_workflow", Description: t("TOOL_RUN_WORKFLOW_DESCRIPTION", "Run an Actions workflow by workflow ID or filename"), + Meta: NewToolMeta(ToolsetMetadataActions, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_RUN_WORKFLOW_USER_TITLE", "Run workflow"), ReadOnlyHint: false, @@ -350,6 +354,7 @@ func GetWorkflowRun(getClient GetClientFn, t translations.TranslationHelperFunc) return mcp.Tool{ Name: "get_workflow_run", Description: t("TOOL_GET_WORKFLOW_RUN_DESCRIPTION", "Get details of a specific workflow run"), + Meta: NewToolMeta(ToolsetMetadataActions, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_GET_WORKFLOW_RUN_USER_TITLE", "Get workflow run"), ReadOnlyHint: true, @@ -413,6 +418,7 @@ func GetWorkflowRunLogs(getClient GetClientFn, t translations.TranslationHelperF return mcp.Tool{ Name: "get_workflow_run_logs", Description: t("TOOL_GET_WORKFLOW_RUN_LOGS_DESCRIPTION", "Download logs for a specific workflow run (EXPENSIVE: downloads ALL logs as ZIP. Consider using get_job_logs with failed_only=true for debugging failed jobs)"), + Meta: NewToolMeta(ToolsetMetadataActions, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_GET_WORKFLOW_RUN_LOGS_USER_TITLE", "Get workflow run logs"), ReadOnlyHint: true, @@ -486,6 +492,7 @@ func ListWorkflowJobs(getClient GetClientFn, t translations.TranslationHelperFun return mcp.Tool{ Name: "list_workflow_jobs", Description: t("TOOL_LIST_WORKFLOW_JOBS_DESCRIPTION", "List jobs for a specific workflow run"), + Meta: NewToolMeta(ToolsetMetadataActions, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_LIST_WORKFLOW_JOBS_USER_TITLE", "List workflow jobs"), ReadOnlyHint: true, @@ -581,6 +588,7 @@ func GetJobLogs(getClient GetClientFn, t translations.TranslationHelperFunc, con return mcp.Tool{ Name: "get_job_logs", Description: t("TOOL_GET_JOB_LOGS_DESCRIPTION", "Download logs for a specific workflow job or efficiently get all failed job logs for a workflow run"), + Meta: NewToolMeta(ToolsetMetadataActions, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_GET_JOB_LOGS_USER_TITLE", "Get job logs"), ReadOnlyHint: true, @@ -841,6 +849,7 @@ func RerunWorkflowRun(getClient GetClientFn, t translations.TranslationHelperFun return mcp.Tool{ Name: "rerun_workflow_run", Description: t("TOOL_RERUN_WORKFLOW_RUN_DESCRIPTION", "Re-run an entire workflow run"), + Meta: NewToolMeta(ToolsetMetadataActions, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_RERUN_WORKFLOW_RUN_USER_TITLE", "Rerun workflow run"), ReadOnlyHint: false, @@ -911,6 +920,7 @@ func RerunFailedJobs(getClient GetClientFn, t translations.TranslationHelperFunc return mcp.Tool{ Name: "rerun_failed_jobs", Description: t("TOOL_RERUN_FAILED_JOBS_DESCRIPTION", "Re-run only the failed jobs in a workflow run"), + Meta: NewToolMeta(ToolsetMetadataActions, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_RERUN_FAILED_JOBS_USER_TITLE", "Rerun failed jobs"), ReadOnlyHint: false, @@ -981,6 +991,7 @@ func CancelWorkflowRun(getClient GetClientFn, t translations.TranslationHelperFu return mcp.Tool{ Name: "cancel_workflow_run", Description: t("TOOL_CANCEL_WORKFLOW_RUN_DESCRIPTION", "Cancel a workflow run"), + Meta: NewToolMeta(ToolsetMetadataActions, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_CANCEL_WORKFLOW_RUN_USER_TITLE", "Cancel workflow run"), ReadOnlyHint: false, @@ -1053,6 +1064,7 @@ func ListWorkflowRunArtifacts(getClient GetClientFn, t translations.TranslationH return mcp.Tool{ Name: "list_workflow_run_artifacts", Description: t("TOOL_LIST_WORKFLOW_RUN_ARTIFACTS_DESCRIPTION", "List artifacts for a workflow run"), + Meta: NewToolMeta(ToolsetMetadataActions, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_LIST_WORKFLOW_RUN_ARTIFACTS_USER_TITLE", "List workflow artifacts"), ReadOnlyHint: true, @@ -1128,6 +1140,7 @@ func DownloadWorkflowRunArtifact(getClient GetClientFn, t translations.Translati return mcp.Tool{ Name: "download_workflow_run_artifact", Description: t("TOOL_DOWNLOAD_WORKFLOW_RUN_ARTIFACT_DESCRIPTION", "Get download URL for a workflow run artifact"), + Meta: NewToolMeta(ToolsetMetadataActions, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_DOWNLOAD_WORKFLOW_RUN_ARTIFACT_USER_TITLE", "Download workflow artifact"), ReadOnlyHint: true, @@ -1200,6 +1213,7 @@ func DeleteWorkflowRunLogs(getClient GetClientFn, t translations.TranslationHelp return mcp.Tool{ Name: "delete_workflow_run_logs", Description: t("TOOL_DELETE_WORKFLOW_RUN_LOGS_DESCRIPTION", "Delete logs for a workflow run"), + Meta: NewToolMeta(ToolsetMetadataActions, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_DELETE_WORKFLOW_RUN_LOGS_USER_TITLE", "Delete workflow logs"), ReadOnlyHint: false, @@ -1271,6 +1285,7 @@ func GetWorkflowRunUsage(getClient GetClientFn, t translations.TranslationHelper return mcp.Tool{ Name: "get_workflow_run_usage", Description: t("TOOL_GET_WORKFLOW_RUN_USAGE_DESCRIPTION", "Get usage metrics for a workflow run"), + Meta: NewToolMeta(ToolsetMetadataActions, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_GET_WORKFLOW_RUN_USAGE_USER_TITLE", "Get workflow usage"), ReadOnlyHint: true, diff --git a/pkg/github/code_scanning.go b/pkg/github/code_scanning.go index 0f8e2780b..9f90fb13c 100644 --- a/pkg/github/code_scanning.go +++ b/pkg/github/code_scanning.go @@ -8,6 +8,7 @@ import ( "net/http" ghErrors "github.com/github/github-mcp-server/pkg/errors" + "github.com/github/github-mcp-server/pkg/scopes" "github.com/github/github-mcp-server/pkg/translations" "github.com/github/github-mcp-server/pkg/utils" "github.com/google/go-github/v79/github" @@ -19,6 +20,7 @@ func GetCodeScanningAlert(getClient GetClientFn, t translations.TranslationHelpe return mcp.Tool{ Name: "get_code_scanning_alert", Description: t("TOOL_GET_CODE_SCANNING_ALERT_DESCRIPTION", "Get details of a specific code scanning alert in a GitHub repository."), + Meta: NewToolMeta(ToolsetMetadataCodeSecurity, scopes.SecurityEvents), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_GET_CODE_SCANNING_ALERT_USER_TITLE", "Get code scanning alert"), ReadOnlyHint: true, @@ -92,6 +94,7 @@ func ListCodeScanningAlerts(getClient GetClientFn, t translations.TranslationHel return mcp.Tool{ Name: "list_code_scanning_alerts", Description: t("TOOL_LIST_CODE_SCANNING_ALERTS_DESCRIPTION", "List code scanning alerts in a GitHub repository."), + Meta: NewToolMeta(ToolsetMetadataCodeSecurity, scopes.SecurityEvents), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_LIST_CODE_SCANNING_ALERTS_USER_TITLE", "List code scanning alerts"), ReadOnlyHint: true, diff --git a/pkg/github/context_tools.go b/pkg/github/context_tools.go index 3fe622379..51e978fb3 100644 --- a/pkg/github/context_tools.go +++ b/pkg/github/context_tools.go @@ -6,6 +6,7 @@ import ( "time" ghErrors "github.com/github/github-mcp-server/pkg/errors" + "github.com/github/github-mcp-server/pkg/scopes" "github.com/github/github-mcp-server/pkg/translations" "github.com/github/github-mcp-server/pkg/utils" "github.com/google/jsonschema-go/jsonschema" @@ -40,6 +41,7 @@ func GetMe(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Too return mcp.Tool{ Name: "get_me", Description: t("TOOL_GET_ME_DESCRIPTION", "Get details of the authenticated GitHub user. Use this when a request is about the user's own profile for GitHub. Or when information is missing to build other tool calls."), + Meta: NewToolMeta(ToolsetMetadataContext), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_GET_ME_USER_TITLE", "Get my user profile"), ReadOnlyHint: true, @@ -109,6 +111,7 @@ func GetTeams(getClient GetClientFn, getGQLClient GetGQLClientFn, t translations return mcp.Tool{ Name: "get_teams", Description: t("TOOL_GET_TEAMS_DESCRIPTION", "Get details of the teams the user is a member of. Limited to organizations accessible with current credentials"), + Meta: NewToolMeta(ToolsetMetadataContext, scopes.ReadOrg), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_GET_TEAMS_TITLE", "Get teams"), ReadOnlyHint: true, @@ -203,6 +206,7 @@ func GetTeamMembers(getGQLClient GetGQLClientFn, t translations.TranslationHelpe return mcp.Tool{ Name: "get_team_members", Description: t("TOOL_GET_TEAM_MEMBERS_DESCRIPTION", "Get member usernames of a specific team in an organization. Limited to organizations accessible with current credentials"), + Meta: NewToolMeta(ToolsetMetadataContext, scopes.ReadOrg), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_GET_TEAM_MEMBERS_TITLE", "Get team members"), ReadOnlyHint: true, diff --git a/pkg/github/dependabot.go b/pkg/github/dependabot.go index 351cbdb37..77e3d819d 100644 --- a/pkg/github/dependabot.go +++ b/pkg/github/dependabot.go @@ -8,6 +8,7 @@ import ( "net/http" ghErrors "github.com/github/github-mcp-server/pkg/errors" + "github.com/github/github-mcp-server/pkg/scopes" "github.com/github/github-mcp-server/pkg/translations" "github.com/github/github-mcp-server/pkg/utils" "github.com/google/go-github/v79/github" @@ -19,6 +20,7 @@ func GetDependabotAlert(getClient GetClientFn, t translations.TranslationHelperF tool := mcp.Tool{ Name: "get_dependabot_alert", Description: t("TOOL_GET_DEPENDABOT_ALERT_DESCRIPTION", "Get details of a specific dependabot alert in a GitHub repository."), + Meta: NewToolMeta(ToolsetMetadataDependabot, scopes.SecurityEvents), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_GET_DEPENDABOT_ALERT_USER_TITLE", "Get dependabot alert"), ReadOnlyHint: true, @@ -95,6 +97,7 @@ func ListDependabotAlerts(getClient GetClientFn, t translations.TranslationHelpe tool := mcp.Tool{ Name: "list_dependabot_alerts", Description: t("TOOL_LIST_DEPENDABOT_ALERTS_DESCRIPTION", "List dependabot alerts in a GitHub repository."), + Meta: NewToolMeta(ToolsetMetadataDependabot, scopes.SecurityEvents), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_LIST_DEPENDABOT_ALERTS_USER_TITLE", "List dependabot alerts"), ReadOnlyHint: true, diff --git a/pkg/github/discussions.go b/pkg/github/discussions.go index 8a5019701..5641e974c 100644 --- a/pkg/github/discussions.go +++ b/pkg/github/discussions.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/github/github-mcp-server/pkg/scopes" "github.com/github/github-mcp-server/pkg/translations" "github.com/github/github-mcp-server/pkg/utils" "github.com/go-viper/mapstructure/v2" @@ -125,6 +126,7 @@ func ListDiscussions(getGQLClient GetGQLClientFn, t translations.TranslationHelp return mcp.Tool{ Name: "list_discussions", Description: t("TOOL_LIST_DISCUSSIONS_DESCRIPTION", "List discussions for a repository or organisation."), + Meta: NewToolMeta(ToolsetMetadataDiscussions, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_LIST_DISCUSSIONS_USER_TITLE", "List discussions"), ReadOnlyHint: true, @@ -274,6 +276,7 @@ func GetDiscussion(getGQLClient GetGQLClientFn, t translations.TranslationHelper return mcp.Tool{ Name: "get_discussion", Description: t("TOOL_GET_DISCUSSION_DESCRIPTION", "Get a specific discussion by ID"), + Meta: NewToolMeta(ToolsetMetadataDiscussions, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_GET_DISCUSSION_USER_TITLE", "Get discussion"), ReadOnlyHint: true, @@ -374,6 +377,7 @@ func GetDiscussionComments(getGQLClient GetGQLClientFn, t translations.Translati return mcp.Tool{ Name: "get_discussion_comments", Description: t("TOOL_GET_DISCUSSION_COMMENTS_DESCRIPTION", "Get comments from a discussion"), + Meta: NewToolMeta(ToolsetMetadataDiscussions, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_GET_DISCUSSION_COMMENTS_USER_TITLE", "Get discussion comments"), ReadOnlyHint: true, @@ -497,6 +501,7 @@ func ListDiscussionCategories(getGQLClient GetGQLClientFn, t translations.Transl return mcp.Tool{ Name: "list_discussion_categories", Description: t("TOOL_LIST_DISCUSSION_CATEGORIES_DESCRIPTION", "List discussion categories with their id and name, for a repository or organisation."), + Meta: NewToolMeta(ToolsetMetadataDiscussions, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_LIST_DISCUSSION_CATEGORIES_USER_TITLE", "List discussion categories"), ReadOnlyHint: true, diff --git a/pkg/github/gists.go b/pkg/github/gists.go index b54553aac..a8a803292 100644 --- a/pkg/github/gists.go +++ b/pkg/github/gists.go @@ -7,6 +7,7 @@ import ( "io" "net/http" + "github.com/github/github-mcp-server/pkg/scopes" "github.com/github/github-mcp-server/pkg/translations" "github.com/github/github-mcp-server/pkg/utils" "github.com/google/go-github/v79/github" @@ -19,6 +20,7 @@ func ListGists(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp tool := mcp.Tool{ Name: "list_gists", Description: t("TOOL_LIST_GISTS_DESCRIPTION", "List gists for a user"), + Meta: NewToolMeta(ToolsetMetadataGists), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_LIST_GISTS", "List Gists"), ReadOnlyHint: true, @@ -105,6 +107,7 @@ func GetGist(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.T tool := mcp.Tool{ Name: "get_gist", Description: t("TOOL_GET_GIST_DESCRIPTION", "Get gist content of a particular gist, by gist ID"), + Meta: NewToolMeta(ToolsetMetadataGists), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_GET_GIST", "Get Gist Content"), ReadOnlyHint: true, @@ -162,6 +165,7 @@ func CreateGist(getClient GetClientFn, t translations.TranslationHelperFunc) (mc tool := mcp.Tool{ Name: "create_gist", Description: t("TOOL_CREATE_GIST_DESCRIPTION", "Create a new gist"), + Meta: NewToolMeta(ToolsetMetadataGists, scopes.Gist), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_CREATE_GIST", "Create Gist"), ReadOnlyHint: false, @@ -264,6 +268,7 @@ func UpdateGist(getClient GetClientFn, t translations.TranslationHelperFunc) (mc tool := mcp.Tool{ Name: "update_gist", Description: t("TOOL_UPDATE_GIST_DESCRIPTION", "Update an existing gist"), + Meta: NewToolMeta(ToolsetMetadataGists, scopes.Gist), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_UPDATE_GIST", "Update Gist"), ReadOnlyHint: false, diff --git a/pkg/github/git.go b/pkg/github/git.go index c2a839132..cff1450d0 100644 --- a/pkg/github/git.go +++ b/pkg/github/git.go @@ -7,6 +7,7 @@ import ( "strings" ghErrors "github.com/github/github-mcp-server/pkg/errors" + "github.com/github/github-mcp-server/pkg/scopes" "github.com/github/github-mcp-server/pkg/translations" "github.com/github/github-mcp-server/pkg/utils" "github.com/google/go-github/v79/github" @@ -41,6 +42,7 @@ func GetRepositoryTree(getClient GetClientFn, t translations.TranslationHelperFu tool := mcp.Tool{ Name: "get_repository_tree", Description: t("TOOL_GET_REPOSITORY_TREE_DESCRIPTION", "Get the tree structure (files and directories) of a GitHub repository at a specific ref or SHA"), + Meta: NewToolMeta(ToolsetMetadataGit, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_GET_REPOSITORY_TREE_USER_TITLE", "Get repository tree"), ReadOnlyHint: true, diff --git a/pkg/github/issues.go b/pkg/github/issues.go index ec83e4efa..b8249ff1a 100644 --- a/pkg/github/issues.go +++ b/pkg/github/issues.go @@ -12,6 +12,7 @@ import ( ghErrors "github.com/github/github-mcp-server/pkg/errors" "github.com/github/github-mcp-server/pkg/lockdown" "github.com/github/github-mcp-server/pkg/sanitize" + "github.com/github/github-mcp-server/pkg/scopes" "github.com/github/github-mcp-server/pkg/translations" "github.com/github/github-mcp-server/pkg/utils" "github.com/go-viper/mapstructure/v2" @@ -264,6 +265,7 @@ Options are: return mcp.Tool{ Name: "issue_read", Description: t("TOOL_ISSUE_READ_DESCRIPTION", "Get information about a specific issue in a GitHub repository."), + Meta: NewToolMeta(ToolsetMetadataIssues, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_ISSUE_READ_USER_TITLE", "Get issue details"), ReadOnlyHint: true, @@ -544,6 +546,7 @@ func ListIssueTypes(getClient GetClientFn, t translations.TranslationHelperFunc) return mcp.Tool{ Name: "list_issue_types", Description: t("TOOL_LIST_ISSUE_TYPES_FOR_ORG", "List supported issue types for repository owner (organization)."), + Meta: NewToolMeta(ToolsetMetadataIssues, scopes.ReadOrg), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_LIST_ISSUE_TYPES_USER_TITLE", "List available issue types"), ReadOnlyHint: true, @@ -597,6 +600,7 @@ func AddIssueComment(getClient GetClientFn, t translations.TranslationHelperFunc return mcp.Tool{ Name: "add_issue_comment", Description: t("TOOL_ADD_ISSUE_COMMENT_DESCRIPTION", "Add a comment to a specific issue in a GitHub repository. Use this tool to add comments to pull requests as well (in this case pass pull request number as issue_number), but only if user is not asking specifically to add review comments."), + Meta: NewToolMeta(ToolsetMetadataIssues, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_ADD_ISSUE_COMMENT_USER_TITLE", "Add comment to issue"), ReadOnlyHint: false, @@ -678,6 +682,7 @@ func SubIssueWrite(getClient GetClientFn, t translations.TranslationHelperFunc) return mcp.Tool{ Name: "sub_issue_write", Description: t("TOOL_SUB_ISSUE_WRITE_DESCRIPTION", "Add a sub-issue to a parent issue in a GitHub repository."), + Meta: NewToolMeta(ToolsetMetadataIssues, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_SUB_ISSUE_WRITE_USER_TITLE", "Change sub-issue"), ReadOnlyHint: false, @@ -945,6 +950,7 @@ func SearchIssues(getClient GetClientFn, t translations.TranslationHelperFunc) ( return mcp.Tool{ Name: "search_issues", Description: t("TOOL_SEARCH_ISSUES_DESCRIPTION", "Search for issues in GitHub repositories using issues search syntax already scoped to is:issue"), + Meta: NewToolMeta(ToolsetMetadataIssues, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_SEARCH_ISSUES_USER_TITLE", "Search issues"), ReadOnlyHint: true, @@ -962,6 +968,7 @@ func IssueWrite(getClient GetClientFn, getGQLClient GetGQLClientFn, t translatio return mcp.Tool{ Name: "issue_write", Description: t("TOOL_ISSUE_WRITE_DESCRIPTION", "Create a new or update an existing issue in a GitHub repository."), + Meta: NewToolMeta(ToolsetMetadataIssues, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_ISSUE_WRITE_USER_TITLE", "Create or update issue."), ReadOnlyHint: false, @@ -1359,6 +1366,7 @@ func ListIssues(getGQLClient GetGQLClientFn, t translations.TranslationHelperFun return mcp.Tool{ Name: "list_issues", Description: t("TOOL_LIST_ISSUES_DESCRIPTION", "List issues in a GitHub repository. For pagination, use the 'endCursor' from the previous response's 'pageInfo' in the 'after' parameter."), + Meta: NewToolMeta(ToolsetMetadataIssues, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_LIST_ISSUES_USER_TITLE", "List issues"), ReadOnlyHint: true, @@ -1591,6 +1599,7 @@ func AssignCopilotToIssue(getGQLClient GetGQLClientFn, t translations.Translatio return mcp.Tool{ Name: "assign_copilot_to_issue", Description: t("TOOL_ASSIGN_COPILOT_TO_ISSUE_DESCRIPTION", description.String()), + Meta: NewToolMeta(ToolsetMetadataIssues, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_ASSIGN_COPILOT_TO_ISSUE_USER_TITLE", "Assign Copilot to issue"), ReadOnlyHint: false, @@ -1774,6 +1783,7 @@ func parseISOTimestamp(timestamp string) (time.Time, error) { func AssignCodingAgentPrompt(t translations.TranslationHelperFunc) (mcp.Prompt, mcp.PromptHandler) { return mcp.Prompt{ + Meta: NewPromptMeta(ToolsetMetadataIssues), Name: "AssignCodingAgent", Description: t("PROMPT_ASSIGN_CODING_AGENT_DESCRIPTION", "Assign GitHub Coding Agent to multiple tasks in a GitHub repository."), Arguments: []*mcp.PromptArgument{ diff --git a/pkg/github/labels.go b/pkg/github/labels.go index 25ac9f7fe..08c1ea677 100644 --- a/pkg/github/labels.go +++ b/pkg/github/labels.go @@ -7,6 +7,7 @@ import ( "strings" ghErrors "github.com/github/github-mcp-server/pkg/errors" + "github.com/github/github-mcp-server/pkg/scopes" "github.com/github/github-mcp-server/pkg/translations" "github.com/github/github-mcp-server/pkg/utils" "github.com/google/jsonschema-go/jsonschema" @@ -19,6 +20,7 @@ func GetLabel(getGQLClient GetGQLClientFn, t translations.TranslationHelperFunc) tool := mcp.Tool{ Name: "get_label", Description: t("TOOL_GET_LABEL_DESCRIPTION", "Get a specific label from a repository."), + Meta: NewToolMeta(ToolsetLabels, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_GET_LABEL_TITLE", "Get a specific label from a repository."), ReadOnlyHint: true, @@ -112,6 +114,7 @@ func ListLabels(getGQLClient GetGQLClientFn, t translations.TranslationHelperFun tool := mcp.Tool{ Name: "list_label", Description: t("TOOL_LIST_LABEL_DESCRIPTION", "List labels from a repository"), + Meta: NewToolMeta(ToolsetLabels, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_LIST_LABEL_DESCRIPTION", "List labels from a repository."), ReadOnlyHint: true, @@ -202,6 +205,7 @@ func LabelWrite(getGQLClient GetGQLClientFn, t translations.TranslationHelperFun tool := mcp.Tool{ Name: "label_write", Description: t("TOOL_LABEL_WRITE_DESCRIPTION", "Perform write operations on repository labels. To set labels on issues, use the 'update_issue' tool."), + Meta: NewToolMeta(ToolsetLabels, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_LABEL_WRITE_TITLE", "Write operations on repository labels."), ReadOnlyHint: false, diff --git a/pkg/github/notifications.go b/pkg/github/notifications.go index 7f9e98f91..44f844a6b 100644 --- a/pkg/github/notifications.go +++ b/pkg/github/notifications.go @@ -10,6 +10,7 @@ import ( "time" ghErrors "github.com/github/github-mcp-server/pkg/errors" + "github.com/github/github-mcp-server/pkg/scopes" "github.com/github/github-mcp-server/pkg/translations" "github.com/github/github-mcp-server/pkg/utils" "github.com/google/go-github/v79/github" @@ -28,6 +29,7 @@ func ListNotifications(getClient GetClientFn, t translations.TranslationHelperFu return mcp.Tool{ Name: "list_notifications", Description: t("TOOL_LIST_NOTIFICATIONS_DESCRIPTION", "Lists all GitHub notifications for the authenticated user, including unread notifications, mentions, review requests, assignments, and updates on issues or pull requests. Use this tool whenever the user asks what to work on next, requests a summary of their GitHub activity, wants to see pending reviews, or needs to check for new updates or tasks. This tool is the primary way to discover actionable items, reminders, and outstanding work on GitHub. Always call this tool when asked what to work on next, what is pending, or what needs attention in GitHub."), + Meta: NewToolMeta(ToolsetMetadataNotifications, scopes.Notifications), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_LIST_NOTIFICATIONS_USER_TITLE", "List notifications"), ReadOnlyHint: true, @@ -161,6 +163,7 @@ func DismissNotification(getclient GetClientFn, t translations.TranslationHelper return mcp.Tool{ Name: "dismiss_notification", Description: t("TOOL_DISMISS_NOTIFICATION_DESCRIPTION", "Dismiss a notification by marking it as read or done"), + Meta: NewToolMeta(ToolsetMetadataNotifications, scopes.Notifications), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_DISMISS_NOTIFICATION_USER_TITLE", "Dismiss notification"), ReadOnlyHint: false, @@ -239,6 +242,7 @@ func MarkAllNotificationsRead(getClient GetClientFn, t translations.TranslationH return mcp.Tool{ Name: "mark_all_notifications_read", Description: t("TOOL_MARK_ALL_NOTIFICATIONS_READ_DESCRIPTION", "Mark all notifications as read"), + Meta: NewToolMeta(ToolsetMetadataNotifications, scopes.Notifications), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_MARK_ALL_NOTIFICATIONS_READ_USER_TITLE", "Mark all notifications as read"), ReadOnlyHint: false, @@ -327,6 +331,7 @@ func GetNotificationDetails(getClient GetClientFn, t translations.TranslationHel return mcp.Tool{ Name: "get_notification_details", Description: t("TOOL_GET_NOTIFICATION_DETAILS_DESCRIPTION", "Get detailed information for a specific GitHub notification, always call this tool when the user asks for details about a specific notification, if you don't know the ID list notifications first."), + Meta: NewToolMeta(ToolsetMetadataNotifications, scopes.Notifications), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_GET_NOTIFICATION_DETAILS_USER_TITLE", "Get notification details"), ReadOnlyHint: true, @@ -392,6 +397,7 @@ func ManageNotificationSubscription(getClient GetClientFn, t translations.Transl return mcp.Tool{ Name: "manage_notification_subscription", Description: t("TOOL_MANAGE_NOTIFICATION_SUBSCRIPTION_DESCRIPTION", "Manage a notification subscription: ignore, watch, or delete a notification thread subscription."), + Meta: NewToolMeta(ToolsetMetadataNotifications, scopes.Notifications), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_MANAGE_NOTIFICATION_SUBSCRIPTION_USER_TITLE", "Manage notification subscription"), ReadOnlyHint: false, @@ -484,6 +490,7 @@ func ManageRepositoryNotificationSubscription(getClient GetClientFn, t translati return mcp.Tool{ Name: "manage_repository_notification_subscription", Description: t("TOOL_MANAGE_REPOSITORY_NOTIFICATION_SUBSCRIPTION_DESCRIPTION", "Manage a repository notification subscription: ignore, watch, or delete repository notifications subscription for the provided repository."), + Meta: NewToolMeta(ToolsetMetadataNotifications, scopes.Notifications), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_MANAGE_REPOSITORY_NOTIFICATION_SUBSCRIPTION_USER_TITLE", "Manage repository notification subscription"), ReadOnlyHint: false, diff --git a/pkg/github/projects.go b/pkg/github/projects.go index 79dfb25ce..d39a295f8 100644 --- a/pkg/github/projects.go +++ b/pkg/github/projects.go @@ -9,6 +9,7 @@ import ( "strings" ghErrors "github.com/github/github-mcp-server/pkg/errors" + "github.com/github/github-mcp-server/pkg/scopes" "github.com/github/github-mcp-server/pkg/translations" "github.com/github/github-mcp-server/pkg/utils" "github.com/google/go-github/v79/github" @@ -28,6 +29,7 @@ func ListProjects(getClient GetClientFn, t translations.TranslationHelperFunc) ( return mcp.Tool{ Name: "list_projects", Description: t("TOOL_LIST_PROJECTS_DESCRIPTION", `List Projects for a user or organization`), + Meta: NewToolMeta(ToolsetMetadataProjects, scopes.ReadProject), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_LIST_PROJECTS_USER_TITLE", "List projects"), ReadOnlyHint: true, @@ -140,6 +142,7 @@ func GetProject(getClient GetClientFn, t translations.TranslationHelperFunc) (mc return mcp.Tool{ Name: "get_project", Description: t("TOOL_GET_PROJECT_DESCRIPTION", "Get Project for a user or org"), + Meta: NewToolMeta(ToolsetMetadataProjects, scopes.ReadProject), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_GET_PROJECT_USER_TITLE", "Get project"), ReadOnlyHint: true, @@ -224,6 +227,7 @@ func ListProjectFields(getClient GetClientFn, t translations.TranslationHelperFu return mcp.Tool{ Name: "list_project_fields", Description: t("TOOL_LIST_PROJECT_FIELDS_DESCRIPTION", "List Project fields for a user or org"), + Meta: NewToolMeta(ToolsetMetadataProjects, scopes.ReadProject), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_LIST_PROJECT_FIELDS_USER_TITLE", "List project fields"), ReadOnlyHint: true, @@ -325,6 +329,7 @@ func GetProjectField(getClient GetClientFn, t translations.TranslationHelperFunc return mcp.Tool{ Name: "get_project_field", Description: t("TOOL_GET_PROJECT_FIELD_DESCRIPTION", "Get Project field for a user or org"), + Meta: NewToolMeta(ToolsetMetadataProjects, scopes.ReadProject), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_GET_PROJECT_FIELD_USER_TITLE", "Get project field"), ReadOnlyHint: true, @@ -412,6 +417,7 @@ func ListProjectItems(getClient GetClientFn, t translations.TranslationHelperFun return mcp.Tool{ Name: "list_project_items", Description: t("TOOL_LIST_PROJECT_ITEMS_DESCRIPTION", `Search project items with advanced filtering`), + Meta: NewToolMeta(ToolsetMetadataProjects, scopes.ReadProject), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_LIST_PROJECT_ITEMS_USER_TITLE", "List project items"), ReadOnlyHint: true, @@ -543,6 +549,7 @@ func GetProjectItem(getClient GetClientFn, t translations.TranslationHelperFunc) return mcp.Tool{ Name: "get_project_item", Description: t("TOOL_GET_PROJECT_ITEM_DESCRIPTION", "Get a specific Project item for a user or org"), + Meta: NewToolMeta(ToolsetMetadataProjects, scopes.ReadProject), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_GET_PROJECT_ITEM_USER_TITLE", "Get project item"), ReadOnlyHint: true, @@ -644,6 +651,7 @@ func AddProjectItem(getClient GetClientFn, t translations.TranslationHelperFunc) return mcp.Tool{ Name: "add_project_item", Description: t("TOOL_ADD_PROJECT_ITEM_DESCRIPTION", "Add a specific Project item for a user or org"), + Meta: NewToolMeta(ToolsetMetadataProjects, scopes.Project), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_ADD_PROJECT_ITEM_USER_TITLE", "Add project item"), ReadOnlyHint: false, @@ -750,6 +758,7 @@ func UpdateProjectItem(getClient GetClientFn, t translations.TranslationHelperFu return mcp.Tool{ Name: "update_project_item", Description: t("TOOL_UPDATE_PROJECT_ITEM_DESCRIPTION", "Update a specific Project item for a user or org"), + Meta: NewToolMeta(ToolsetMetadataProjects, scopes.Project), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_UPDATE_PROJECT_ITEM_USER_TITLE", "Update project item"), ReadOnlyHint: false, @@ -857,6 +866,7 @@ func DeleteProjectItem(getClient GetClientFn, t translations.TranslationHelperFu return mcp.Tool{ Name: "delete_project_item", Description: t("TOOL_DELETE_PROJECT_ITEM_DESCRIPTION", "Delete a specific Project item for a user or org"), + Meta: NewToolMeta(ToolsetMetadataProjects, scopes.Project), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_DELETE_PROJECT_ITEM_USER_TITLE", "Delete project item"), ReadOnlyHint: false, diff --git a/pkg/github/pullrequests.go b/pkg/github/pullrequests.go index 661384529..c36618db1 100644 --- a/pkg/github/pullrequests.go +++ b/pkg/github/pullrequests.go @@ -16,6 +16,7 @@ import ( ghErrors "github.com/github/github-mcp-server/pkg/errors" "github.com/github/github-mcp-server/pkg/lockdown" "github.com/github/github-mcp-server/pkg/sanitize" + "github.com/github/github-mcp-server/pkg/scopes" "github.com/github/github-mcp-server/pkg/translations" "github.com/github/github-mcp-server/pkg/utils" ) @@ -59,6 +60,7 @@ Possible options: return mcp.Tool{ Name: "pull_request_read", Description: t("TOOL_PULL_REQUEST_READ_DESCRIPTION", "Get information on a specific pull request in GitHub repository."), + Meta: NewToolMeta(ToolsetMetadataPullRequests, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_GET_PULL_REQUEST_USER_TITLE", "Get details for a single pull request"), ReadOnlyHint: true, @@ -428,6 +430,7 @@ func CreatePullRequest(getClient GetClientFn, t translations.TranslationHelperFu return mcp.Tool{ Name: "create_pull_request", Description: t("TOOL_CREATE_PULL_REQUEST_DESCRIPTION", "Create a new pull request in a GitHub repository."), + Meta: NewToolMeta(ToolsetMetadataPullRequests, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_CREATE_PULL_REQUEST_USER_TITLE", "Open new pull request"), ReadOnlyHint: false, @@ -577,6 +580,7 @@ func UpdatePullRequest(getClient GetClientFn, getGQLClient GetGQLClientFn, t tra return mcp.Tool{ Name: "update_pull_request", Description: t("TOOL_UPDATE_PULL_REQUEST_DESCRIPTION", "Update an existing pull request in a GitHub repository."), + Meta: NewToolMeta(ToolsetMetadataPullRequests, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_UPDATE_PULL_REQUEST_USER_TITLE", "Edit pull request"), ReadOnlyHint: false, @@ -856,6 +860,7 @@ func ListPullRequests(getClient GetClientFn, t translations.TranslationHelperFun return mcp.Tool{ Name: "list_pull_requests", Description: t("TOOL_LIST_PULL_REQUESTS_DESCRIPTION", "List pull requests in a GitHub repository. If the user specifies an author, then DO NOT use this tool and use the search_pull_requests tool instead."), + Meta: NewToolMeta(ToolsetMetadataPullRequests, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_LIST_PULL_REQUESTS_USER_TITLE", "List pull requests"), ReadOnlyHint: true, @@ -989,6 +994,7 @@ func MergePullRequest(getClient GetClientFn, t translations.TranslationHelperFun return mcp.Tool{ Name: "merge_pull_request", Description: t("TOOL_MERGE_PULL_REQUEST_DESCRIPTION", "Merge a pull request in a GitHub repository."), + Meta: NewToolMeta(ToolsetMetadataPullRequests, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_MERGE_PULL_REQUEST_USER_TITLE", "Merge pull request"), ReadOnlyHint: false, @@ -1104,6 +1110,7 @@ func SearchPullRequests(getClient GetClientFn, t translations.TranslationHelperF return mcp.Tool{ Name: "search_pull_requests", Description: t("TOOL_SEARCH_PULL_REQUESTS_DESCRIPTION", "Search for pull requests in GitHub repositories using issues search syntax already scoped to is:pr"), + Meta: NewToolMeta(ToolsetMetadataPullRequests, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_SEARCH_PULL_REQUESTS_USER_TITLE", "Search pull requests"), ReadOnlyHint: true, @@ -1144,6 +1151,7 @@ func UpdatePullRequestBranch(getClient GetClientFn, t translations.TranslationHe return mcp.Tool{ Name: "update_pull_request_branch", Description: t("TOOL_UPDATE_PULL_REQUEST_BRANCH_DESCRIPTION", "Update the branch of a pull request with the latest changes from the base branch."), + Meta: NewToolMeta(ToolsetMetadataPullRequests, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_UPDATE_PULL_REQUEST_BRANCH_USER_TITLE", "Update pull request branch"), ReadOnlyHint: false, @@ -1268,6 +1276,7 @@ Available methods: - submit_pending: Submit an existing pending review of a pull request. This requires that a pending review exists for the current user on the specified pull request. The "body" and "event" parameters are used when submitting the review. - delete_pending: Delete an existing pending review of a pull request. This requires that a pending review exists for the current user on the specified pull request. `), + Meta: NewToolMeta(ToolsetMetadataPullRequests, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_PULL_REQUEST_REVIEW_WRITE_USER_TITLE", "Write operations (create, submit, delete) on pull request reviews."), ReadOnlyHint: false, @@ -1589,6 +1598,7 @@ func AddCommentToPendingReview(getGQLClient GetGQLClientFn, t translations.Trans return mcp.Tool{ Name: "add_comment_to_pending_review", Description: t("TOOL_ADD_COMMENT_TO_PENDING_REVIEW_DESCRIPTION", "Add review comment to the requester's latest pending pull request review. A pending review needs to already exist to call this (check with the user if not sure)."), + Meta: NewToolMeta(ToolsetMetadataPullRequests, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_ADD_COMMENT_TO_PENDING_REVIEW_USER_TITLE", "Add review comment to the requester's latest pending pull request review"), ReadOnlyHint: false, @@ -1738,6 +1748,7 @@ func RequestCopilotReview(getClient GetClientFn, t translations.TranslationHelpe return mcp.Tool{ Name: "request_copilot_review", Description: t("TOOL_REQUEST_COPILOT_REVIEW_DESCRIPTION", "Request a GitHub Copilot code review for a pull request. Use this for automated feedback on pull requests, usually before requesting a human reviewer."), + Meta: NewToolMeta(ToolsetMetadataPullRequests, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_REQUEST_COPILOT_REVIEW_USER_TITLE", "Request Copilot review"), ReadOnlyHint: false, diff --git a/pkg/github/repositories.go b/pkg/github/repositories.go index ff81484f2..fe2d06646 100644 --- a/pkg/github/repositories.go +++ b/pkg/github/repositories.go @@ -11,6 +11,7 @@ import ( ghErrors "github.com/github/github-mcp-server/pkg/errors" "github.com/github/github-mcp-server/pkg/raw" + "github.com/github/github-mcp-server/pkg/scopes" "github.com/github/github-mcp-server/pkg/translations" "github.com/github/github-mcp-server/pkg/utils" "github.com/google/go-github/v79/github" @@ -22,6 +23,7 @@ func GetCommit(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp tool := mcp.Tool{ Name: "get_commit", Description: t("TOOL_GET_COMMITS_DESCRIPTION", "Get details for a commit from a GitHub repository"), + Meta: NewToolMeta(ToolsetMetadataRepos, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_GET_COMMITS_USER_TITLE", "Get commit details"), ReadOnlyHint: true, @@ -119,6 +121,7 @@ func ListCommits(getClient GetClientFn, t translations.TranslationHelperFunc) (m tool := mcp.Tool{ Name: "list_commits", Description: t("TOOL_LIST_COMMITS_DESCRIPTION", "Get list of commits of a branch in a GitHub repository. Returns at least 30 results per page by default, but can return more if specified using the perPage parameter (up to 100)."), + Meta: NewToolMeta(ToolsetMetadataRepos, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_LIST_COMMITS_USER_TITLE", "List commits"), ReadOnlyHint: true, @@ -226,6 +229,7 @@ func ListBranches(getClient GetClientFn, t translations.TranslationHelperFunc) ( tool := mcp.Tool{ Name: "list_branches", Description: t("TOOL_LIST_BRANCHES_DESCRIPTION", "List branches in a GitHub repository"), + Meta: NewToolMeta(ToolsetMetadataRepos, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_LIST_BRANCHES_USER_TITLE", "List branches"), ReadOnlyHint: true, @@ -312,6 +316,7 @@ func CreateOrUpdateFile(getClient GetClientFn, t translations.TranslationHelperF tool := mcp.Tool{ Name: "create_or_update_file", Description: t("TOOL_CREATE_OR_UPDATE_FILE_DESCRIPTION", "Create or update a single file in a GitHub repository. If updating, you must provide the SHA of the file you want to update. Use this tool to create or update a file in a GitHub repository remotely; do not use it for local file operations."), + Meta: NewToolMeta(ToolsetMetadataRepos, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_CREATE_OR_UPDATE_FILE_USER_TITLE", "Create or update file"), ReadOnlyHint: false, @@ -438,6 +443,7 @@ func CreateRepository(getClient GetClientFn, t translations.TranslationHelperFun tool := mcp.Tool{ Name: "create_repository", Description: t("TOOL_CREATE_REPOSITORY_DESCRIPTION", "Create a new GitHub repository in your account or specified organization"), + Meta: NewToolMeta(ToolsetMetadataRepos, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_CREATE_REPOSITORY_USER_TITLE", "Create repository"), ReadOnlyHint: false, @@ -543,6 +549,7 @@ func GetFileContents(getClient GetClientFn, getRawClient raw.GetRawClientFn, t t tool := mcp.Tool{ Name: "get_file_contents", Description: t("TOOL_GET_FILE_CONTENTS_DESCRIPTION", "Get the contents of a file or directory from a GitHub repository"), + Meta: NewToolMeta(ToolsetMetadataRepos, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_GET_FILE_CONTENTS_USER_TITLE", "Get file or directory contents"), ReadOnlyHint: true, @@ -761,6 +768,7 @@ func ForkRepository(getClient GetClientFn, t translations.TranslationHelperFunc) tool := mcp.Tool{ Name: "fork_repository", Description: t("TOOL_FORK_REPOSITORY_DESCRIPTION", "Fork a GitHub repository to your account or specified organization"), + Meta: NewToolMeta(ToolsetMetadataRepos, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_FORK_REPOSITORY_USER_TITLE", "Fork repository"), ReadOnlyHint: false, @@ -858,6 +866,7 @@ func DeleteFile(getClient GetClientFn, t translations.TranslationHelperFunc) (mc tool := mcp.Tool{ Name: "delete_file", Description: t("TOOL_DELETE_FILE_DESCRIPTION", "Delete a file from a GitHub repository"), + Meta: NewToolMeta(ToolsetMetadataRepos, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_DELETE_FILE_USER_TITLE", "Delete file"), ReadOnlyHint: false, @@ -1042,6 +1051,7 @@ func CreateBranch(getClient GetClientFn, t translations.TranslationHelperFunc) ( tool := mcp.Tool{ Name: "create_branch", Description: t("TOOL_CREATE_BRANCH_DESCRIPTION", "Create a new branch in a GitHub repository"), + Meta: NewToolMeta(ToolsetMetadataRepos, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_CREATE_BRANCH_USER_TITLE", "Create branch"), ReadOnlyHint: false, @@ -1154,6 +1164,7 @@ func PushFiles(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp tool := mcp.Tool{ Name: "push_files", Description: t("TOOL_PUSH_FILES_DESCRIPTION", "Push multiple files to a GitHub repository in a single commit"), + Meta: NewToolMeta(ToolsetMetadataRepos, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_PUSH_FILES_USER_TITLE", "Push files to repository"), ReadOnlyHint: false, @@ -1337,6 +1348,7 @@ func ListTags(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp. tool := mcp.Tool{ Name: "list_tags", Description: t("TOOL_LIST_TAGS_DESCRIPTION", "List git tags in a GitHub repository"), + Meta: NewToolMeta(ToolsetMetadataRepos, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_LIST_TAGS_USER_TITLE", "List tags"), ReadOnlyHint: true, @@ -1415,6 +1427,7 @@ func GetTag(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.To tool := mcp.Tool{ Name: "get_tag", Description: t("TOOL_GET_TAG_DESCRIPTION", "Get details about a specific git tag in a GitHub repository"), + Meta: NewToolMeta(ToolsetMetadataRepos, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_GET_TAG_USER_TITLE", "Get tag details"), ReadOnlyHint: true, @@ -1512,6 +1525,7 @@ func ListReleases(getClient GetClientFn, t translations.TranslationHelperFunc) ( tool := mcp.Tool{ Name: "list_releases", Description: t("TOOL_LIST_RELEASES_DESCRIPTION", "List releases in a GitHub repository"), + Meta: NewToolMeta(ToolsetMetadataRepos, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_LIST_RELEASES_USER_TITLE", "List releases"), ReadOnlyHint: true, @@ -1586,6 +1600,7 @@ func GetLatestRelease(getClient GetClientFn, t translations.TranslationHelperFun tool := mcp.Tool{ Name: "get_latest_release", Description: t("TOOL_GET_LATEST_RELEASE_DESCRIPTION", "Get the latest release in a GitHub repository"), + Meta: NewToolMeta(ToolsetMetadataRepos, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_GET_LATEST_RELEASE_USER_TITLE", "Get latest release"), ReadOnlyHint: true, @@ -1650,6 +1665,7 @@ func GetReleaseByTag(getClient GetClientFn, t translations.TranslationHelperFunc tool := mcp.Tool{ Name: "get_release_by_tag", Description: t("TOOL_GET_RELEASE_BY_TAG_DESCRIPTION", "Get a specific release by its tag name in a GitHub repository"), + Meta: NewToolMeta(ToolsetMetadataRepos, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_GET_RELEASE_BY_TAG_USER_TITLE", "Get a release by tag name"), ReadOnlyHint: true, @@ -1993,6 +2009,7 @@ func StarRepository(getClient GetClientFn, t translations.TranslationHelperFunc) tool := mcp.Tool{ Name: "star_repository", Description: t("TOOL_STAR_REPOSITORY_DESCRIPTION", "Star a GitHub repository"), + Meta: NewToolMeta(ToolsetMetadataStargazers, scopes.PublicRepo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_STAR_REPOSITORY_USER_TITLE", "Star repository"), ReadOnlyHint: false, @@ -2057,6 +2074,7 @@ func UnstarRepository(getClient GetClientFn, t translations.TranslationHelperFun tool := mcp.Tool{ Name: "unstar_repository", Description: t("TOOL_UNSTAR_REPOSITORY_DESCRIPTION", "Unstar a GitHub repository"), + Meta: NewToolMeta(ToolsetMetadataStargazers, scopes.PublicRepo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_UNSTAR_REPOSITORY_USER_TITLE", "Unstar repository"), ReadOnlyHint: false, diff --git a/pkg/github/repository_resource.go b/pkg/github/repository_resource.go index 5dea9f4e9..08a1566b3 100644 --- a/pkg/github/repository_resource.go +++ b/pkg/github/repository_resource.go @@ -31,6 +31,7 @@ var ( // GetRepositoryResourceContent defines the resource template and handler for getting repository content. func GetRepositoryResourceContent(getClient GetClientFn, getRawClient raw.GetRawClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, mcp.ResourceHandler) { return mcp.ResourceTemplate{ + Meta: NewResourceMeta(ToolsetMetadataRepos), Name: "repository_content", URITemplate: repositoryResourceContentURITemplate.Raw(), // Resource template Description: t("RESOURCE_REPOSITORY_CONTENT_DESCRIPTION", "Repository Content"), @@ -41,6 +42,7 @@ func GetRepositoryResourceContent(getClient GetClientFn, getRawClient raw.GetRaw // GetRepositoryResourceBranchContent defines the resource template and handler for getting repository content for a branch. func GetRepositoryResourceBranchContent(getClient GetClientFn, getRawClient raw.GetRawClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, mcp.ResourceHandler) { return mcp.ResourceTemplate{ + Meta: NewResourceMeta(ToolsetMetadataRepos), Name: "repository_content_branch", URITemplate: repositoryResourceBranchContentURITemplate.Raw(), // Resource template Description: t("RESOURCE_REPOSITORY_CONTENT_BRANCH_DESCRIPTION", "Repository Content for specific branch"), @@ -51,6 +53,7 @@ func GetRepositoryResourceBranchContent(getClient GetClientFn, getRawClient raw. // GetRepositoryResourceCommitContent defines the resource template and handler for getting repository content for a commit. func GetRepositoryResourceCommitContent(getClient GetClientFn, getRawClient raw.GetRawClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, mcp.ResourceHandler) { return mcp.ResourceTemplate{ + Meta: NewResourceMeta(ToolsetMetadataRepos), Name: "repository_content_commit", URITemplate: repositoryResourceCommitContentURITemplate.Raw(), // Resource template Description: t("RESOURCE_REPOSITORY_CONTENT_COMMIT_DESCRIPTION", "Repository Content for specific commit"), @@ -61,6 +64,7 @@ func GetRepositoryResourceCommitContent(getClient GetClientFn, getRawClient raw. // GetRepositoryResourceTagContent defines the resource template and handler for getting repository content for a tag. func GetRepositoryResourceTagContent(getClient GetClientFn, getRawClient raw.GetRawClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, mcp.ResourceHandler) { return mcp.ResourceTemplate{ + Meta: NewResourceMeta(ToolsetMetadataRepos), Name: "repository_content_tag", URITemplate: repositoryResourceTagContentURITemplate.Raw(), // Resource template Description: t("RESOURCE_REPOSITORY_CONTENT_TAG_DESCRIPTION", "Repository Content for specific tag"), @@ -71,6 +75,7 @@ func GetRepositoryResourceTagContent(getClient GetClientFn, getRawClient raw.Get // GetRepositoryResourcePrContent defines the resource template and handler for getting repository content for a pull request. func GetRepositoryResourcePrContent(getClient GetClientFn, getRawClient raw.GetRawClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, mcp.ResourceHandler) { return mcp.ResourceTemplate{ + Meta: NewResourceMeta(ToolsetMetadataRepos), Name: "repository_content_pr", URITemplate: repositoryResourcePrContentURITemplate.Raw(), // Resource template Description: t("RESOURCE_REPOSITORY_CONTENT_PR_DESCRIPTION", "Repository Content for specific pull request"), diff --git a/pkg/github/search.go b/pkg/github/search.go index cffd0bf15..822235735 100644 --- a/pkg/github/search.go +++ b/pkg/github/search.go @@ -8,6 +8,7 @@ import ( "net/http" ghErrors "github.com/github/github-mcp-server/pkg/errors" + "github.com/github/github-mcp-server/pkg/scopes" "github.com/github/github-mcp-server/pkg/translations" "github.com/github/github-mcp-server/pkg/utils" "github.com/google/go-github/v79/github" @@ -47,6 +48,7 @@ func SearchRepositories(getClient GetClientFn, t translations.TranslationHelperF return mcp.Tool{ Name: "search_repositories", Description: t("TOOL_SEARCH_REPOSITORIES_DESCRIPTION", "Find GitHub repositories by name, description, readme, topics, or other metadata. Perfect for discovering projects, finding examples, or locating specific repositories across GitHub."), + Meta: NewToolMeta(ToolsetMetadataRepos, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_SEARCH_REPOSITORIES_USER_TITLE", "Search repositories"), ReadOnlyHint: true, @@ -186,6 +188,7 @@ func SearchCode(getClient GetClientFn, t translations.TranslationHelperFunc) (mc return mcp.Tool{ Name: "search_code", Description: t("TOOL_SEARCH_CODE_DESCRIPTION", "Fast and precise code search across ALL GitHub repositories using GitHub's native search engine. Best for finding exact symbols, functions, classes, or specific code patterns."), + Meta: NewToolMeta(ToolsetMetadataRepos, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_SEARCH_CODE_USER_TITLE", "Search code"), ReadOnlyHint: true, @@ -366,6 +369,7 @@ func SearchUsers(getClient GetClientFn, t translations.TranslationHelperFunc) (m return mcp.Tool{ Name: "search_users", Description: t("TOOL_SEARCH_USERS_DESCRIPTION", "Find GitHub users by username, real name, or other profile information. Useful for locating developers, contributors, or team members."), + Meta: NewToolMeta(ToolsetMetadataUsers, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_SEARCH_USERS_USER_TITLE", "Search users"), ReadOnlyHint: true, @@ -401,6 +405,7 @@ func SearchOrgs(getClient GetClientFn, t translations.TranslationHelperFunc) (mc return mcp.Tool{ Name: "search_orgs", Description: t("TOOL_SEARCH_ORGS_DESCRIPTION", "Find GitHub organizations by name, location, or other organization metadata. Ideal for discovering companies, open source foundations, or teams."), + Meta: NewToolMeta(ToolsetMetadataOrgs, scopes.Repo), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_SEARCH_ORGS_USER_TITLE", "Search organizations"), ReadOnlyHint: true, diff --git a/pkg/github/secret_scanning.go b/pkg/github/secret_scanning.go index 297e1ebfe..99d2ab89e 100644 --- a/pkg/github/secret_scanning.go +++ b/pkg/github/secret_scanning.go @@ -8,6 +8,7 @@ import ( "net/http" ghErrors "github.com/github/github-mcp-server/pkg/errors" + "github.com/github/github-mcp-server/pkg/scopes" "github.com/github/github-mcp-server/pkg/translations" "github.com/github/github-mcp-server/pkg/utils" "github.com/google/go-github/v79/github" @@ -19,6 +20,7 @@ func GetSecretScanningAlert(getClient GetClientFn, t translations.TranslationHel return mcp.Tool{ Name: "get_secret_scanning_alert", Description: t("TOOL_GET_SECRET_SCANNING_ALERT_DESCRIPTION", "Get details of a specific secret scanning alert in a GitHub repository."), + Meta: NewToolMeta(ToolsetMetadataSecretProtection, scopes.SecurityEvents), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_GET_SECRET_SCANNING_ALERT_USER_TITLE", "Get secret scanning alert"), ReadOnlyHint: true, @@ -92,6 +94,7 @@ func ListSecretScanningAlerts(getClient GetClientFn, t translations.TranslationH return mcp.Tool{ Name: "list_secret_scanning_alerts", Description: t("TOOL_LIST_SECRET_SCANNING_ALERTS_DESCRIPTION", "List secret scanning alerts in a GitHub repository."), + Meta: NewToolMeta(ToolsetMetadataSecretProtection, scopes.SecurityEvents), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_LIST_SECRET_SCANNING_ALERTS_USER_TITLE", "List secret scanning alerts"), ReadOnlyHint: true, diff --git a/pkg/github/security_advisories.go b/pkg/github/security_advisories.go index 58148a7a3..f38db2c5f 100644 --- a/pkg/github/security_advisories.go +++ b/pkg/github/security_advisories.go @@ -7,6 +7,7 @@ import ( "io" "net/http" + "github.com/github/github-mcp-server/pkg/scopes" "github.com/github/github-mcp-server/pkg/translations" "github.com/github/github-mcp-server/pkg/utils" "github.com/google/go-github/v79/github" @@ -18,6 +19,7 @@ func ListGlobalSecurityAdvisories(getClient GetClientFn, t translations.Translat tool := mcp.Tool{ Name: "list_global_security_advisories", Description: t("TOOL_LIST_GLOBAL_SECURITY_ADVISORIES_DESCRIPTION", "List global security advisories from GitHub."), + Meta: NewToolMeta(ToolsetMetadataSecurityAdvisories, scopes.SecurityEvents), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_LIST_GLOBAL_SECURITY_ADVISORIES_USER_TITLE", "List global security advisories"), ReadOnlyHint: true, @@ -208,6 +210,7 @@ func ListRepositorySecurityAdvisories(getClient GetClientFn, t translations.Tran tool := mcp.Tool{ Name: "list_repository_security_advisories", Description: t("TOOL_LIST_REPOSITORY_SECURITY_ADVISORIES_DESCRIPTION", "List repository security advisories for a GitHub repository."), + Meta: NewToolMeta(ToolsetMetadataSecurityAdvisories, scopes.SecurityEvents), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_LIST_REPOSITORY_SECURITY_ADVISORIES_USER_TITLE", "List repository security advisories"), ReadOnlyHint: true, @@ -311,6 +314,7 @@ func GetGlobalSecurityAdvisory(getClient GetClientFn, t translations.Translation tool := mcp.Tool{ Name: "get_global_security_advisory", Description: t("TOOL_GET_GLOBAL_SECURITY_ADVISORY_DESCRIPTION", "Get a global security advisory"), + Meta: NewToolMeta(ToolsetMetadataSecurityAdvisories, scopes.SecurityEvents), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_GET_GLOBAL_SECURITY_ADVISORY_USER_TITLE", "Get a global security advisory"), ReadOnlyHint: true, @@ -367,6 +371,7 @@ func ListOrgRepositorySecurityAdvisories(getClient GetClientFn, t translations.T tool := mcp.Tool{ Name: "list_org_repository_security_advisories", Description: t("TOOL_LIST_ORG_REPOSITORY_SECURITY_ADVISORIES_DESCRIPTION", "List repository security advisories for a GitHub organization."), + Meta: NewToolMeta(ToolsetMetadataSecurityAdvisories, scopes.SecurityEvents), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_LIST_ORG_REPOSITORY_SECURITY_ADVISORIES_USER_TITLE", "List org repository security advisories"), ReadOnlyHint: true, diff --git a/pkg/github/tools.go b/pkg/github/tools.go index f21a9ae5b..bddb4a91a 100644 --- a/pkg/github/tools.go +++ b/pkg/github/tools.go @@ -7,6 +7,7 @@ import ( "github.com/github/github-mcp-server/pkg/lockdown" "github.com/github/github-mcp-server/pkg/raw" + "github.com/github/github-mcp-server/pkg/scopes" "github.com/github/github-mcp-server/pkg/toolsets" "github.com/github/github-mcp-server/pkg/translations" "github.com/google/go-github/v79/github" @@ -17,10 +18,47 @@ import ( type GetClientFn func(context.Context) (*github.Client, error) type GetGQLClientFn func(context.Context) (*githubv4.Client, error) -// ToolsetMetadata holds metadata for a toolset including its ID and description -type ToolsetMetadata struct { - ID string - Description string +// ToolsetMetadata is an alias to toolsets.ToolsetMetadata for convenience +type ToolsetMetadata = toolsets.ToolsetMetadata + +// NewToolMeta creates tool metadata with the given toolset (required) and scopes. +// Returns mcp.Meta (map[string]any) for direct use in mcp.Tool.Meta. +func NewToolMeta(toolset ToolsetMetadata, requiredScopes ...scopes.Scope) mcp.Meta { + if toolset.ID == "" { + panic("toolset ID is required for ToolMeta") + } + meta := mcp.Meta{"toolset": toolset.ID} + + // Filter out NoScope and collect required scope strings + var scopeStrs []string + for _, s := range requiredScopes { + if s != scopes.NoScope { + scopeStrs = append(scopeStrs, s.String()) + } + } + if len(scopeStrs) > 0 { + meta[scopes.MetaKey] = scopeStrs + } + + return meta +} + +// NewResourceMeta creates resource template metadata with the given toolset. +// Returns mcp.Meta (map[string]any) for direct use in mcp.ResourceTemplate.Meta. +func NewResourceMeta(toolset ToolsetMetadata) mcp.Meta { + if toolset.ID == "" { + panic("toolset ID is required for ResourceMeta") + } + return mcp.Meta{"toolset": toolset.ID} +} + +// NewPromptMeta creates prompt metadata with the given toolset. +// Returns mcp.Meta (map[string]any) for direct use in mcp.Prompt.Meta. +func NewPromptMeta(toolset ToolsetMetadata) mcp.Meta { + if toolset.ID == "" { + panic("toolset ID is required for PromptMeta") + } + return mcp.Meta{"toolset": toolset.ID} } var ( @@ -114,8 +152,8 @@ var ( } ) -func AvailableTools() []ToolsetMetadata { - return []ToolsetMetadata{ +func AvailableToolsets() []toolsets.ToolsetMetadata { + return []toolsets.ToolsetMetadata{ ToolsetMetadataContext, ToolsetMetadataRepos, ToolsetMetadataIssues, @@ -133,7 +171,6 @@ func AvailableTools() []ToolsetMetadata { ToolsetMetadataSecurityAdvisories, ToolsetMetadataProjects, ToolsetMetadataStargazers, - ToolsetMetadataDynamic, ToolsetLabels, } } @@ -141,7 +178,7 @@ func AvailableTools() []ToolsetMetadata { // GetValidToolsetIDs returns a map of all valid toolset IDs for quick lookup func GetValidToolsetIDs() map[string]bool { validIDs := make(map[string]bool) - for _, tool := range AvailableTools() { + for _, tool := range AvailableToolsets() { validIDs[tool.ID] = true } // Add special keywords @@ -160,223 +197,173 @@ func GetDefaultToolsetIDs() []string { } } -func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetGQLClientFn, getRawClient raw.GetRawClientFn, t translations.TranslationHelperFunc, contentWindowSize int, flags FeatureFlags, cache *lockdown.RepoAccessCache) *toolsets.ToolsetGroup { - tsg := toolsets.NewToolsetGroup(readOnly) - - // Define all available features with their default state (disabled) - // Create toolsets - repos := toolsets.NewToolset(ToolsetMetadataRepos.ID, ToolsetMetadataRepos.Description). - AddReadTools( - toolsets.NewServerTool(SearchRepositories(getClient, t)), - toolsets.NewServerTool(GetFileContents(getClient, getRawClient, t)), - toolsets.NewServerTool(ListCommits(getClient, t)), - toolsets.NewServerTool(SearchCode(getClient, t)), - toolsets.NewServerTool(GetCommit(getClient, t)), - toolsets.NewServerTool(ListBranches(getClient, t)), - toolsets.NewServerTool(ListTags(getClient, t)), - toolsets.NewServerTool(GetTag(getClient, t)), - toolsets.NewServerTool(ListReleases(getClient, t)), - toolsets.NewServerTool(GetLatestRelease(getClient, t)), - toolsets.NewServerTool(GetReleaseByTag(getClient, t)), - ). - AddWriteTools( - toolsets.NewServerTool(CreateOrUpdateFile(getClient, t)), - toolsets.NewServerTool(CreateRepository(getClient, t)), - toolsets.NewServerTool(ForkRepository(getClient, t)), - toolsets.NewServerTool(CreateBranch(getClient, t)), - toolsets.NewServerTool(PushFiles(getClient, t)), - toolsets.NewServerTool(DeleteFile(getClient, t)), - ). - AddResourceTemplates( - toolsets.NewServerResourceTemplate(GetRepositoryResourceContent(getClient, getRawClient, t)), - toolsets.NewServerResourceTemplate(GetRepositoryResourceBranchContent(getClient, getRawClient, t)), - toolsets.NewServerResourceTemplate(GetRepositoryResourceCommitContent(getClient, getRawClient, t)), - toolsets.NewServerResourceTemplate(GetRepositoryResourceTagContent(getClient, getRawClient, t)), - toolsets.NewServerResourceTemplate(GetRepositoryResourcePrContent(getClient, getRawClient, t)), - ) - git := toolsets.NewToolset(ToolsetMetadataGit.ID, ToolsetMetadataGit.Description). - AddReadTools( - toolsets.NewServerTool(GetRepositoryTree(getClient, t)), - ) - issues := toolsets.NewToolset(ToolsetMetadataIssues.ID, ToolsetMetadataIssues.Description). - AddReadTools( - toolsets.NewServerTool(IssueRead(getClient, getGQLClient, cache, t, flags)), - toolsets.NewServerTool(SearchIssues(getClient, t)), - toolsets.NewServerTool(ListIssues(getGQLClient, t)), - toolsets.NewServerTool(ListIssueTypes(getClient, t)), - toolsets.NewServerTool(GetLabel(getGQLClient, t)), - ). - AddWriteTools( - toolsets.NewServerTool(IssueWrite(getClient, getGQLClient, t)), - toolsets.NewServerTool(AddIssueComment(getClient, t)), - toolsets.NewServerTool(AssignCopilotToIssue(getGQLClient, t)), - toolsets.NewServerTool(SubIssueWrite(getClient, t)), - ).AddPrompts( +// NewDefaultToolsetRegistry creates a ToolsetRegistry with all available GitHub tools. +// The registry can then be used to create ToolsetGroups with different configurations. +func NewDefaultToolsetRegistry(getClient GetClientFn, getGQLClient GetGQLClientFn, getRawClient raw.GetRawClientFn, t translations.TranslationHelperFunc, contentWindowSize int, flags FeatureFlags, cache *lockdown.RepoAccessCache) *toolsets.ToolsetRegistry { + // Collect all tools - they are self-describing with toolset and read/write info in Meta and Annotations + tools := []toolsets.ServerTool{ + // Context tools + toolsets.NewServerTool(GetMe(getClient, t)), + toolsets.NewServerTool(GetTeams(getClient, getGQLClient, t)), + toolsets.NewServerTool(GetTeamMembers(getGQLClient, t)), + + // Repository tools + toolsets.NewServerTool(SearchRepositories(getClient, t)), + toolsets.NewServerTool(GetFileContents(getClient, getRawClient, t)), + toolsets.NewServerTool(ListCommits(getClient, t)), + toolsets.NewServerTool(SearchCode(getClient, t)), + toolsets.NewServerTool(GetCommit(getClient, t)), + toolsets.NewServerTool(ListBranches(getClient, t)), + toolsets.NewServerTool(ListTags(getClient, t)), + toolsets.NewServerTool(GetTag(getClient, t)), + toolsets.NewServerTool(ListReleases(getClient, t)), + toolsets.NewServerTool(GetLatestRelease(getClient, t)), + toolsets.NewServerTool(GetReleaseByTag(getClient, t)), + toolsets.NewServerTool(CreateOrUpdateFile(getClient, t)), + toolsets.NewServerTool(CreateRepository(getClient, t)), + toolsets.NewServerTool(ForkRepository(getClient, t)), + toolsets.NewServerTool(CreateBranch(getClient, t)), + toolsets.NewServerTool(PushFiles(getClient, t)), + toolsets.NewServerTool(DeleteFile(getClient, t)), + + // Git tools + toolsets.NewServerTool(GetRepositoryTree(getClient, t)), + + // Issue tools + toolsets.NewServerTool(IssueRead(getClient, getGQLClient, cache, t, flags)), + toolsets.NewServerTool(SearchIssues(getClient, t)), + toolsets.NewServerTool(ListIssues(getGQLClient, t)), + toolsets.NewServerTool(ListIssueTypes(getClient, t)), + toolsets.NewServerTool(IssueWrite(getClient, getGQLClient, t)), + toolsets.NewServerTool(AddIssueComment(getClient, t)), + toolsets.NewServerTool(AssignCopilotToIssue(getGQLClient, t)), + toolsets.NewServerTool(SubIssueWrite(getClient, t)), + + // User tools + toolsets.NewServerTool(SearchUsers(getClient, t)), + + // Org tools + toolsets.NewServerTool(SearchOrgs(getClient, t)), + + // Pull request tools + toolsets.NewServerTool(PullRequestRead(getClient, cache, t, flags)), + toolsets.NewServerTool(ListPullRequests(getClient, t)), + toolsets.NewServerTool(SearchPullRequests(getClient, t)), + toolsets.NewServerTool(MergePullRequest(getClient, t)), + toolsets.NewServerTool(UpdatePullRequestBranch(getClient, t)), + toolsets.NewServerTool(CreatePullRequest(getClient, t)), + toolsets.NewServerTool(UpdatePullRequest(getClient, getGQLClient, t)), + toolsets.NewServerTool(RequestCopilotReview(getClient, t)), + toolsets.NewServerTool(PullRequestReviewWrite(getGQLClient, t)), + toolsets.NewServerTool(AddCommentToPendingReview(getGQLClient, t)), + + // Code security tools + toolsets.NewServerTool(GetCodeScanningAlert(getClient, t)), + toolsets.NewServerTool(ListCodeScanningAlerts(getClient, t)), + + // Secret protection tools + toolsets.NewServerTool(GetSecretScanningAlert(getClient, t)), + toolsets.NewServerTool(ListSecretScanningAlerts(getClient, t)), + + // Dependabot tools + toolsets.NewServerTool(GetDependabotAlert(getClient, t)), + toolsets.NewServerTool(ListDependabotAlerts(getClient, t)), + + // Notification tools + toolsets.NewServerTool(ListNotifications(getClient, t)), + toolsets.NewServerTool(GetNotificationDetails(getClient, t)), + toolsets.NewServerTool(DismissNotification(getClient, t)), + toolsets.NewServerTool(MarkAllNotificationsRead(getClient, t)), + toolsets.NewServerTool(ManageNotificationSubscription(getClient, t)), + toolsets.NewServerTool(ManageRepositoryNotificationSubscription(getClient, t)), + + // Discussion tools + toolsets.NewServerTool(ListDiscussions(getGQLClient, t)), + toolsets.NewServerTool(GetDiscussion(getGQLClient, t)), + toolsets.NewServerTool(GetDiscussionComments(getGQLClient, t)), + toolsets.NewServerTool(ListDiscussionCategories(getGQLClient, t)), + + // Actions tools + toolsets.NewServerTool(ListWorkflows(getClient, t)), + toolsets.NewServerTool(ListWorkflowRuns(getClient, t)), + toolsets.NewServerTool(GetWorkflowRun(getClient, t)), + toolsets.NewServerTool(GetWorkflowRunLogs(getClient, t)), + toolsets.NewServerTool(ListWorkflowJobs(getClient, t)), + toolsets.NewServerTool(GetJobLogs(getClient, t, contentWindowSize)), + toolsets.NewServerTool(ListWorkflowRunArtifacts(getClient, t)), + toolsets.NewServerTool(DownloadWorkflowRunArtifact(getClient, t)), + toolsets.NewServerTool(GetWorkflowRunUsage(getClient, t)), + toolsets.NewServerTool(RunWorkflow(getClient, t)), + toolsets.NewServerTool(RerunWorkflowRun(getClient, t)), + toolsets.NewServerTool(RerunFailedJobs(getClient, t)), + toolsets.NewServerTool(CancelWorkflowRun(getClient, t)), + toolsets.NewServerTool(DeleteWorkflowRunLogs(getClient, t)), + + // Security advisories tools + toolsets.NewServerTool(ListGlobalSecurityAdvisories(getClient, t)), + toolsets.NewServerTool(GetGlobalSecurityAdvisory(getClient, t)), + toolsets.NewServerTool(ListRepositorySecurityAdvisories(getClient, t)), + toolsets.NewServerTool(ListOrgRepositorySecurityAdvisories(getClient, t)), + + // Gist tools + toolsets.NewServerTool(ListGists(getClient, t)), + toolsets.NewServerTool(GetGist(getClient, t)), + toolsets.NewServerTool(CreateGist(getClient, t)), + toolsets.NewServerTool(UpdateGist(getClient, t)), + + // Project tools + toolsets.NewServerTool(ListProjects(getClient, t)), + toolsets.NewServerTool(GetProject(getClient, t)), + toolsets.NewServerTool(ListProjectFields(getClient, t)), + toolsets.NewServerTool(GetProjectField(getClient, t)), + toolsets.NewServerTool(ListProjectItems(getClient, t)), + toolsets.NewServerTool(GetProjectItem(getClient, t)), + toolsets.NewServerTool(AddProjectItem(getClient, t)), + toolsets.NewServerTool(DeleteProjectItem(getClient, t)), + toolsets.NewServerTool(UpdateProjectItem(getClient, t)), + + // Stargazer tools + toolsets.NewServerTool(ListStarredRepositories(getClient, t)), + toolsets.NewServerTool(StarRepository(getClient, t)), + toolsets.NewServerTool(UnstarRepository(getClient, t)), + + // Label tools + toolsets.NewServerTool(GetLabel(getGQLClient, t)), + toolsets.NewServerTool(ListLabels(getGQLClient, t)), + toolsets.NewServerTool(LabelWrite(getGQLClient, t)), + } + + // Include all available toolsets plus experiments (which has no tools but needs to exist) + toolsetMetadatas := append(AvailableToolsets(), ToolsetMetadataExperiments) + + // Resource templates - self-describing with toolset in Meta + resourceTemplates := []toolsets.ServerResourceTemplate{ + toolsets.NewServerResourceTemplate(GetRepositoryResourceContent(getClient, getRawClient, t)), + toolsets.NewServerResourceTemplate(GetRepositoryResourceBranchContent(getClient, getRawClient, t)), + toolsets.NewServerResourceTemplate(GetRepositoryResourceCommitContent(getClient, getRawClient, t)), + toolsets.NewServerResourceTemplate(GetRepositoryResourceTagContent(getClient, getRawClient, t)), + toolsets.NewServerResourceTemplate(GetRepositoryResourcePrContent(getClient, getRawClient, t)), + } + + // Prompts - self-describing with toolset in Meta + prompts := []toolsets.ServerPrompt{ toolsets.NewServerPrompt(AssignCodingAgentPrompt(t)), toolsets.NewServerPrompt(IssueToFixWorkflowPrompt(t)), - ) - users := toolsets.NewToolset(ToolsetMetadataUsers.ID, ToolsetMetadataUsers.Description). - AddReadTools( - toolsets.NewServerTool(SearchUsers(getClient, t)), - ) - orgs := toolsets.NewToolset(ToolsetMetadataOrgs.ID, ToolsetMetadataOrgs.Description). - AddReadTools( - toolsets.NewServerTool(SearchOrgs(getClient, t)), - ) - pullRequests := toolsets.NewToolset(ToolsetMetadataPullRequests.ID, ToolsetMetadataPullRequests.Description). - AddReadTools( - toolsets.NewServerTool(PullRequestRead(getClient, cache, t, flags)), - toolsets.NewServerTool(ListPullRequests(getClient, t)), - toolsets.NewServerTool(SearchPullRequests(getClient, t)), - ). - AddWriteTools( - toolsets.NewServerTool(MergePullRequest(getClient, t)), - toolsets.NewServerTool(UpdatePullRequestBranch(getClient, t)), - toolsets.NewServerTool(CreatePullRequest(getClient, t)), - toolsets.NewServerTool(UpdatePullRequest(getClient, getGQLClient, t)), - toolsets.NewServerTool(RequestCopilotReview(getClient, t)), - // Reviews - toolsets.NewServerTool(PullRequestReviewWrite(getGQLClient, t)), - toolsets.NewServerTool(AddCommentToPendingReview(getGQLClient, t)), - ) - codeSecurity := toolsets.NewToolset(ToolsetMetadataCodeSecurity.ID, ToolsetMetadataCodeSecurity.Description). - AddReadTools( - toolsets.NewServerTool(GetCodeScanningAlert(getClient, t)), - toolsets.NewServerTool(ListCodeScanningAlerts(getClient, t)), - ) - secretProtection := toolsets.NewToolset(ToolsetMetadataSecretProtection.ID, ToolsetMetadataSecretProtection.Description). - AddReadTools( - toolsets.NewServerTool(GetSecretScanningAlert(getClient, t)), - toolsets.NewServerTool(ListSecretScanningAlerts(getClient, t)), - ) - dependabot := toolsets.NewToolset(ToolsetMetadataDependabot.ID, ToolsetMetadataDependabot.Description). - AddReadTools( - toolsets.NewServerTool(GetDependabotAlert(getClient, t)), - toolsets.NewServerTool(ListDependabotAlerts(getClient, t)), - ) - - notifications := toolsets.NewToolset(ToolsetMetadataNotifications.ID, ToolsetMetadataNotifications.Description). - AddReadTools( - toolsets.NewServerTool(ListNotifications(getClient, t)), - toolsets.NewServerTool(GetNotificationDetails(getClient, t)), - ). - AddWriteTools( - toolsets.NewServerTool(DismissNotification(getClient, t)), - toolsets.NewServerTool(MarkAllNotificationsRead(getClient, t)), - toolsets.NewServerTool(ManageNotificationSubscription(getClient, t)), - toolsets.NewServerTool(ManageRepositoryNotificationSubscription(getClient, t)), - ) - - discussions := toolsets.NewToolset(ToolsetMetadataDiscussions.ID, ToolsetMetadataDiscussions.Description). - AddReadTools( - toolsets.NewServerTool(ListDiscussions(getGQLClient, t)), - toolsets.NewServerTool(GetDiscussion(getGQLClient, t)), - toolsets.NewServerTool(GetDiscussionComments(getGQLClient, t)), - toolsets.NewServerTool(ListDiscussionCategories(getGQLClient, t)), - ) - - actions := toolsets.NewToolset(ToolsetMetadataActions.ID, ToolsetMetadataActions.Description). - AddReadTools( - toolsets.NewServerTool(ListWorkflows(getClient, t)), - toolsets.NewServerTool(ListWorkflowRuns(getClient, t)), - toolsets.NewServerTool(GetWorkflowRun(getClient, t)), - toolsets.NewServerTool(GetWorkflowRunLogs(getClient, t)), - toolsets.NewServerTool(ListWorkflowJobs(getClient, t)), - toolsets.NewServerTool(GetJobLogs(getClient, t, contentWindowSize)), - toolsets.NewServerTool(ListWorkflowRunArtifacts(getClient, t)), - toolsets.NewServerTool(DownloadWorkflowRunArtifact(getClient, t)), - toolsets.NewServerTool(GetWorkflowRunUsage(getClient, t)), - ). - AddWriteTools( - toolsets.NewServerTool(RunWorkflow(getClient, t)), - toolsets.NewServerTool(RerunWorkflowRun(getClient, t)), - toolsets.NewServerTool(RerunFailedJobs(getClient, t)), - toolsets.NewServerTool(CancelWorkflowRun(getClient, t)), - toolsets.NewServerTool(DeleteWorkflowRunLogs(getClient, t)), - ) - - securityAdvisories := toolsets.NewToolset(ToolsetMetadataSecurityAdvisories.ID, ToolsetMetadataSecurityAdvisories.Description). - AddReadTools( - toolsets.NewServerTool(ListGlobalSecurityAdvisories(getClient, t)), - toolsets.NewServerTool(GetGlobalSecurityAdvisory(getClient, t)), - toolsets.NewServerTool(ListRepositorySecurityAdvisories(getClient, t)), - toolsets.NewServerTool(ListOrgRepositorySecurityAdvisories(getClient, t)), - ) - - // // Keep experiments alive so the system doesn't error out when it's always enabled - experiments := toolsets.NewToolset(ToolsetMetadataExperiments.ID, ToolsetMetadataExperiments.Description) - - contextTools := toolsets.NewToolset(ToolsetMetadataContext.ID, ToolsetMetadataContext.Description). - AddReadTools( - toolsets.NewServerTool(GetMe(getClient, t)), - toolsets.NewServerTool(GetTeams(getClient, getGQLClient, t)), - toolsets.NewServerTool(GetTeamMembers(getGQLClient, t)), - ) - - gists := toolsets.NewToolset(ToolsetMetadataGists.ID, ToolsetMetadataGists.Description). - AddReadTools( - toolsets.NewServerTool(ListGists(getClient, t)), - toolsets.NewServerTool(GetGist(getClient, t)), - ). - AddWriteTools( - toolsets.NewServerTool(CreateGist(getClient, t)), - toolsets.NewServerTool(UpdateGist(getClient, t)), - ) + } - projects := toolsets.NewToolset(ToolsetMetadataProjects.ID, ToolsetMetadataProjects.Description). - AddReadTools( - toolsets.NewServerTool(ListProjects(getClient, t)), - toolsets.NewServerTool(GetProject(getClient, t)), - toolsets.NewServerTool(ListProjectFields(getClient, t)), - toolsets.NewServerTool(GetProjectField(getClient, t)), - toolsets.NewServerTool(ListProjectItems(getClient, t)), - toolsets.NewServerTool(GetProjectItem(getClient, t)), - ). - AddWriteTools( - toolsets.NewServerTool(AddProjectItem(getClient, t)), - toolsets.NewServerTool(DeleteProjectItem(getClient, t)), - toolsets.NewServerTool(UpdateProjectItem(getClient, t)), - ) - stargazers := toolsets.NewToolset(ToolsetMetadataStargazers.ID, ToolsetMetadataStargazers.Description). - AddReadTools( - toolsets.NewServerTool(ListStarredRepositories(getClient, t)), - ). - AddWriteTools( - toolsets.NewServerTool(StarRepository(getClient, t)), - toolsets.NewServerTool(UnstarRepository(getClient, t)), - ) - labels := toolsets.NewToolset(ToolsetLabels.ID, ToolsetLabels.Description). - AddReadTools( - // get - toolsets.NewServerTool(GetLabel(getGQLClient, t)), - // list labels on repo or issue - toolsets.NewServerTool(ListLabels(getGQLClient, t)), - ). - AddWriteTools( - // create or update - toolsets.NewServerTool(LabelWrite(getGQLClient, t)), - ) + return toolsets.NewToolsetRegistry(toolsetMetadatas, tools). + WithResourceTemplates(resourceTemplates...). + WithPrompts(prompts...) +} - // Add toolsets to the group - tsg.AddToolset(contextTools) - tsg.AddToolset(repos) - tsg.AddToolset(git) - tsg.AddToolset(issues) - tsg.AddToolset(orgs) - tsg.AddToolset(users) - tsg.AddToolset(pullRequests) - tsg.AddToolset(actions) - tsg.AddToolset(codeSecurity) - tsg.AddToolset(dependabot) - tsg.AddToolset(secretProtection) - tsg.AddToolset(notifications) - tsg.AddToolset(experiments) - tsg.AddToolset(discussions) - tsg.AddToolset(gists) - tsg.AddToolset(securityAdvisories) - tsg.AddToolset(projects) - tsg.AddToolset(stargazers) - tsg.AddToolset(labels) +// DefaultToolsetGroup creates a ToolsetGroup with the default configuration. +// This is a convenience wrapper around NewDefaultToolsetRegistry for backwards compatibility. +func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetGQLClientFn, getRawClient raw.GetRawClientFn, t translations.TranslationHelperFunc, contentWindowSize int, flags FeatureFlags, cache *lockdown.RepoAccessCache) *toolsets.ToolsetGroup { + registry := NewDefaultToolsetRegistry(getClient, getGQLClient, getRawClient, t, contentWindowSize, flags, cache) + tsg := registry.NewToolsetGroup(toolsets.ToolsetGroupConfig{ + ReadOnly: readOnly, + AvailableScopes: nil, // No scope filtering for backwards compatibility + }) tsg.AddDeprecatedToolAliases(DeprecatedToolAliases) @@ -420,30 +407,30 @@ func GenerateToolsetsHelp() string { defaultTools := strings.Join(GetDefaultToolsetIDs(), ", ") // Format available tools with line breaks for better readability - allTools := AvailableTools() - var availableToolsLines []string + allToolsets := AvailableToolsets() + var availableToolsetsLines []string const maxLineLength = 70 currentLine := "" - for i, tool := range allTools { + for i, toolset := range allToolsets { switch { case i == 0: - currentLine = tool.ID - case len(currentLine)+len(tool.ID)+2 <= maxLineLength: - currentLine += ", " + tool.ID + currentLine = toolset.ID + case len(currentLine)+len(toolset.ID)+2 <= maxLineLength: + currentLine += ", " + toolset.ID default: - availableToolsLines = append(availableToolsLines, currentLine) - currentLine = tool.ID + availableToolsetsLines = append(availableToolsetsLines, currentLine) + currentLine = toolset.ID } } if currentLine != "" { - availableToolsLines = append(availableToolsLines, currentLine) + availableToolsetsLines = append(availableToolsetsLines, currentLine) } - availableTools := strings.Join(availableToolsLines, ",\n\t ") + availableToolsets := strings.Join(availableToolsetsLines, ",\n\t ") toolsetsHelp := fmt.Sprintf("Comma-separated list of tool groups to enable (no spaces).\n"+ - "Available: %s\n", availableTools) + + "Available: %s\n", availableToolsets) + "Special toolset keywords:\n" + " - all: Enables all available toolsets\n" + fmt.Sprintf(" - default: Enables the default toolset configuration of:\n\t %s\n", defaultTools) + diff --git a/pkg/github/workflow_prompts.go b/pkg/github/workflow_prompts.go index bc7c7581f..c6cc9630c 100644 --- a/pkg/github/workflow_prompts.go +++ b/pkg/github/workflow_prompts.go @@ -11,6 +11,7 @@ import ( // IssueToFixWorkflowPrompt provides a guided workflow for creating an issue and then generating a PR to fix it func IssueToFixWorkflowPrompt(t translations.TranslationHelperFunc) (tool mcp.Prompt, handler mcp.PromptHandler) { return mcp.Prompt{ + Meta: NewPromptMeta(ToolsetMetadataIssues), Name: "issue_to_fix_workflow", Description: t("PROMPT_ISSUE_TO_FIX_WORKFLOW_DESCRIPTION", "Create an issue for a problem and then generate a pull request to fix it"), Arguments: []*mcp.PromptArgument{ diff --git a/pkg/scopes/scopes.go b/pkg/scopes/scopes.go new file mode 100644 index 000000000..13d6064d3 --- /dev/null +++ b/pkg/scopes/scopes.go @@ -0,0 +1,285 @@ +// Package scopes provides OAuth scope definitions and utilities for the GitHub MCP Server. +// These scopes correspond to GitHub OAuth app scopes as documented at: +// https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/scopes-for-oauth-apps +package scopes + +// Scope represents a GitHub OAuth scope. +type Scope string + +// OAuth scope constants based on GitHub's OAuth app scopes. +// See: https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/scopes-for-oauth-apps +const ( + // NoScope grants read-only access to public information (including user profile info, + // repository info, and gists). + NoScope Scope = "" + + // Repo grants full access to public and private repositories including read and write + // access to code, commit statuses, repository invitations, collaborators, deployment + // statuses, and repository webhooks. + Repo Scope = "repo" + + // RepoStatus grants read/write access to commit statuses in public and private repositories. + RepoStatus Scope = "repo:status" + + // RepoDeployment grants access to deployment statuses for public and private repositories. + RepoDeployment Scope = "repo_deployment" + + // PublicRepo limits access to public repositories. That includes read/write access to code, + // commit statuses, repository projects, collaborators, and deployment statuses for public + // repositories and organizations. + PublicRepo Scope = "public_repo" + + // RepoInvite grants accept/decline abilities for invitations to collaborate on a repository. + RepoInvite Scope = "repo:invite" + + // SecurityEvents grants read and write access to security events in the code scanning API. + SecurityEvents Scope = "security_events" + + // AdminRepoHook grants read, write, ping, and delete access to repository hooks. + AdminRepoHook Scope = "admin:repo_hook" + + // WriteRepoHook grants read, write, and ping access to hooks in public or private repositories. + WriteRepoHook Scope = "write:repo_hook" + + // ReadRepoHook grants read and ping access to hooks in public or private repositories. + ReadRepoHook Scope = "read:repo_hook" + + // AdminOrg grants full management of the organization and its teams, projects, and memberships. + AdminOrg Scope = "admin:org" + + // WriteOrg grants read and write access to organization membership and organization projects. + WriteOrg Scope = "write:org" + + // ReadOrg grants read-only access to organization membership, organization projects, and team membership. + ReadOrg Scope = "read:org" + + // AdminPublicKey grants full management of public keys. + AdminPublicKey Scope = "admin:public_key" + + // WritePublicKey grants create, list, and view details for public keys. + WritePublicKey Scope = "write:public_key" + + // ReadPublicKey grants list and view details for public keys. + ReadPublicKey Scope = "read:public_key" + + // AdminOrgHook grants read, write, ping, and delete access to organization hooks. + AdminOrgHook Scope = "admin:org_hook" + + // Gist grants write access to gists. + Gist Scope = "gist" + + // Notifications grants read access to a user's notifications, mark as read access to threads, + // watch and unwatch access to a repository, and read, write, and delete access to thread subscriptions. + Notifications Scope = "notifications" + + // User grants read/write access to profile info only. + User Scope = "user" + + // ReadUser grants access to read a user's profile data. + ReadUser Scope = "read:user" + + // UserEmail grants read access to a user's email addresses. + UserEmail Scope = "user:email" + + // UserFollow grants access to follow or unfollow other users. + UserFollow Scope = "user:follow" + + // Project grants read/write access to user and organization projects. + Project Scope = "project" + + // ReadProject grants read only access to user and organization projects. + ReadProject Scope = "read:project" + + // DeleteRepo grants access to delete adminable repositories. + DeleteRepo Scope = "delete_repo" + + // WritePackages grants access to upload or publish a package in GitHub Packages. + WritePackages Scope = "write:packages" + + // ReadPackages grants access to download or install packages from GitHub Packages. + ReadPackages Scope = "read:packages" + + // DeletePackages grants access to delete packages from GitHub Packages. + DeletePackages Scope = "delete:packages" + + // AdminGPGKey grants full management of GPG keys. + AdminGPGKey Scope = "admin:gpg_key" + + // WriteGPGKey grants create, list, and view details for GPG keys. + WriteGPGKey Scope = "write:gpg_key" + + // ReadGPGKey grants list and view details for GPG keys. + ReadGPGKey Scope = "read:gpg_key" + + // Codespace grants the ability to create and manage codespaces. + Codespace Scope = "codespace" + + // Workflow grants the ability to add and update GitHub Actions workflow files. + Workflow Scope = "workflow" + + // ReadAuditLog grants read access to audit log data. + ReadAuditLog Scope = "read:audit_log" +) + +// String returns the string representation of the scope. +func (s Scope) String() string { + return string(s) +} + +// ScopeHierarchy defines which scopes include other scopes. +// For example, "repo" includes "repo:status", "repo_deployment", "public_repo", and "repo:invite". +// When a user has "repo" scope, they automatically have access to all included scopes. +var ScopeHierarchy = map[Scope][]Scope{ + Repo: {RepoStatus, RepoDeployment, PublicRepo, RepoInvite, SecurityEvents}, + User: {ReadUser, UserEmail, UserFollow}, + + AdminRepoHook: {WriteRepoHook, ReadRepoHook}, + WriteRepoHook: {ReadRepoHook}, + + AdminOrg: {WriteOrg, ReadOrg}, + WriteOrg: {ReadOrg}, + + AdminPublicKey: {WritePublicKey, ReadPublicKey}, + WritePublicKey: {ReadPublicKey}, + + AdminGPGKey: {WriteGPGKey, ReadGPGKey}, + WriteGPGKey: {ReadGPGKey}, + + Project: {ReadProject}, + + WritePackages: {ReadPackages}, +} + +// GetAcceptedScopes returns all scopes that satisfy the given required scope. +// This includes the scope itself plus any parent scopes that include it. +func GetAcceptedScopes(required Scope) []Scope { + accepted := []Scope{required} + seen := make(map[Scope]bool) + seen[required] = true + + // Recursively find all parent scopes + var findParents func(Scope) + findParents = func(child Scope) { + for parent, children := range ScopeHierarchy { + for _, c := range children { + if c == child && !seen[parent] { + seen[parent] = true + accepted = append(accepted, parent) + findParents(parent) // Recursively find parents of this parent + } + } + } + } + findParents(required) + + return accepted +} + +// ScopeIncludes checks if a scope includes another scope (directly or through hierarchy). +func ScopeIncludes(have, need Scope) bool { + if have == need { + return true + } + + // Check if 'have' directly includes 'need' + if children, ok := ScopeHierarchy[have]; ok { + for _, child := range children { + if child == need { + return true + } + // Recursively check + if ScopeIncludes(child, need) { + return true + } + } + } + + return false +} + +// HasRequiredScopes checks if the given scopes satisfy all required scopes. +func HasRequiredScopes(have []Scope, required []Scope) bool { + for _, req := range required { + found := false + for _, h := range have { + if ScopeIncludes(h, req) { + found = true + break + } + } + if !found { + return false + } + } + return true +} + +// ScopeStrings converts a slice of Scope to a slice of strings. +func ScopeStrings(scopes []Scope) []string { + result := make([]string, len(scopes)) + for i, s := range scopes { + result[i] = s.String() + } + return result +} + +// ParseScopes converts a slice of strings to a slice of Scope. +func ParseScopes(strs []string) []Scope { + result := make([]Scope, len(strs)) + for i, s := range strs { + result[i] = Scope(s) + } + return result +} + +// MetaKey is the key used to store OAuth scopes in the mcp.Tool.Meta field. +const MetaKey = "requiredOAuthScopes" + +// WithScopes returns a Meta map containing the required OAuth scopes. +// This is used when defining an mcp.Tool to specify the required scopes. +// +// Example usage: +// +// tool := mcp.Tool{ +// Name: "get_issue", +// Meta: scopes.WithScopes(scopes.Repo), +// ... +// } +func WithScopes(requiredScopes ...Scope) map[string]any { + scopeStrings := make([]string, len(requiredScopes)) + for i, s := range requiredScopes { + scopeStrings[i] = s.String() + } + return map[string]any{ + MetaKey: scopeStrings, + } +} + +// GetScopesFromMeta extracts the required OAuth scopes from an mcp.Tool.Meta field. +// Returns nil if no scopes are defined. +func GetScopesFromMeta(meta map[string]any) []Scope { + if meta == nil { + return nil + } + + scopesVal, ok := meta[MetaKey] + if !ok { + return nil + } + + // Handle both []string and []any (from JSON unmarshaling) + switch v := scopesVal.(type) { + case []string: + return ParseScopes(v) + case []any: + strs := make([]string, len(v)) + for i, s := range v { + if str, ok := s.(string); ok { + strs[i] = str + } + } + return ParseScopes(strs) + default: + return nil + } +} diff --git a/pkg/scopes/scopes_test.go b/pkg/scopes/scopes_test.go new file mode 100644 index 000000000..0b48bb16b --- /dev/null +++ b/pkg/scopes/scopes_test.go @@ -0,0 +1,240 @@ +package scopes + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestScopeString(t *testing.T) { + tests := []struct { + scope Scope + expected string + }{ + {Repo, "repo"}, + {PublicRepo, "public_repo"}, + {Notifications, "notifications"}, + {Gist, "gist"}, + {NoScope, ""}, + {SecurityEvents, "security_events"}, + {ReadOrg, "read:org"}, + {Project, "project"}, + {ReadProject, "read:project"}, + } + + for _, tt := range tests { + t.Run(string(tt.scope), func(t *testing.T) { + assert.Equal(t, tt.expected, tt.scope.String()) + }) + } +} + +func TestScopeIncludes(t *testing.T) { + tests := []struct { + name string + have Scope + need Scope + expected bool + }{ + {"same scope", Repo, Repo, true}, + {"repo includes public_repo", Repo, PublicRepo, true}, + {"repo includes repo:status", Repo, RepoStatus, true}, + {"repo includes security_events", Repo, SecurityEvents, true}, + {"public_repo does not include repo", PublicRepo, Repo, false}, + {"user includes read:user", User, ReadUser, true}, + {"user includes user:email", User, UserEmail, true}, + {"project includes read:project", Project, ReadProject, true}, + {"read:project does not include project", ReadProject, Project, false}, + {"admin:org includes write:org", AdminOrg, WriteOrg, true}, + {"admin:org includes read:org", AdminOrg, ReadOrg, true}, + {"write:org includes read:org", WriteOrg, ReadOrg, true}, + {"unrelated scopes", Gist, Notifications, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ScopeIncludes(tt.have, tt.need) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestHasRequiredScopes(t *testing.T) { + tests := []struct { + name string + have []Scope + required []Scope + expected bool + }{ + { + name: "exact match single scope", + have: []Scope{Repo}, + required: []Scope{Repo}, + expected: true, + }, + { + name: "parent scope satisfies child", + have: []Scope{Repo}, + required: []Scope{PublicRepo}, + expected: true, + }, + { + name: "multiple required all satisfied", + have: []Scope{Repo, Notifications}, + required: []Scope{PublicRepo, Notifications}, + expected: true, + }, + { + name: "missing required scope", + have: []Scope{Repo}, + required: []Scope{Notifications}, + expected: false, + }, + { + name: "empty required", + have: []Scope{Repo}, + required: []Scope{}, + expected: true, + }, + { + name: "empty have with required", + have: []Scope{}, + required: []Scope{Repo}, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := HasRequiredScopes(tt.have, tt.required) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestWithScopes(t *testing.T) { + tests := []struct { + name string + scopes []Scope + expected []string + }{ + { + name: "single scope", + scopes: []Scope{Repo}, + expected: []string{"repo"}, + }, + { + name: "multiple scopes", + scopes: []Scope{Repo, Notifications}, + expected: []string{"repo", "notifications"}, + }, + { + name: "no scope", + scopes: []Scope{NoScope}, + expected: []string{""}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + meta := WithScopes(tt.scopes...) + require.NotNil(t, meta) + + scopeVal, ok := meta[MetaKey] + require.True(t, ok) + + scopeStrings, ok := scopeVal.([]string) + require.True(t, ok) + assert.Equal(t, tt.expected, scopeStrings) + }) + } +} + +func TestGetScopesFromMeta(t *testing.T) { + tests := []struct { + name string + meta map[string]any + expected []Scope + }{ + { + name: "nil meta", + meta: nil, + expected: nil, + }, + { + name: "empty meta", + meta: map[string]any{}, + expected: nil, + }, + { + name: "string slice", + meta: map[string]any{ + MetaKey: []string{"repo", "notifications"}, + }, + expected: []Scope{Repo, Notifications}, + }, + { + name: "any slice (from JSON)", + meta: map[string]any{ + MetaKey: []any{"repo", "gist"}, + }, + expected: []Scope{Repo, Gist}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := GetScopesFromMeta(tt.meta) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestGetAcceptedScopes(t *testing.T) { + tests := []struct { + name string + required Scope + contains []Scope + }{ + { + name: "repo is accepted by itself", + required: Repo, + contains: []Scope{Repo}, + }, + { + name: "public_repo accepted by repo", + required: PublicRepo, + contains: []Scope{PublicRepo, Repo}, + }, + { + name: "read:org accepted by admin:org and write:org", + required: ReadOrg, + contains: []Scope{ReadOrg, WriteOrg, AdminOrg}, + }, + { + name: "read:project accepted by project", + required: ReadProject, + contains: []Scope{ReadProject, Project}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := GetAcceptedScopes(tt.required) + for _, expected := range tt.contains { + assert.Contains(t, result, expected, "expected %s to be in accepted scopes", expected) + } + }) + } +} + +func TestScopeStringsAndParseScopes(t *testing.T) { + original := []Scope{Repo, Notifications, Gist} + strings := ScopeStrings(original) + + assert.Equal(t, []string{"repo", "notifications", "gist"}, strings) + + parsed := ParseScopes(strings) + assert.Equal(t, original, parsed) +} diff --git a/pkg/toolsets/toolsets.go b/pkg/toolsets/toolsets.go index d96b5fb50..db86ac0be 100644 --- a/pkg/toolsets/toolsets.go +++ b/pkg/toolsets/toolsets.go @@ -207,6 +207,241 @@ func NewToolsetGroup(readOnly bool) *ToolsetGroup { } } +// ToolsetMetadata holds metadata for a toolset including its ID and description +type ToolsetMetadata struct { + ID string + Description string +} + +// ToolsetRegistry holds a collection of toolset definitions and their tools, resources, and prompts. +// It provides a NewToolsetGroup method to create configured ToolsetGroups. +type ToolsetRegistry struct { + toolsetMetadatas []ToolsetMetadata + tools []ServerTool + resourceTemplates []ServerResourceTemplate + prompts []ServerPrompt +} + +// NewToolsetRegistry creates a new ToolsetRegistry with the given toolset metadata and tools. +func NewToolsetRegistry(toolsetMetadatas []ToolsetMetadata, tools []ServerTool) *ToolsetRegistry { + return &ToolsetRegistry{ + toolsetMetadatas: toolsetMetadatas, + tools: tools, + } +} + +// WithResourceTemplates adds resource templates to the registry. +func (r *ToolsetRegistry) WithResourceTemplates(templates ...ServerResourceTemplate) *ToolsetRegistry { + r.resourceTemplates = append(r.resourceTemplates, templates...) + return r +} + +// WithPrompts adds prompts to the registry. +func (r *ToolsetRegistry) WithPrompts(prompts ...ServerPrompt) *ToolsetRegistry { + r.prompts = append(r.prompts, prompts...) + return r +} + +// ToolsetGroupConfig specifies the configuration for creating a ToolsetGroup. +type ToolsetGroupConfig struct { + // ReadOnly when true restricts the group to read-only tools + ReadOnly bool + // ActiveToolsets specifies which toolsets should be enabled. + // If nil, no toolsets are enabled by default. + ActiveToolsets []string + // AvailableScopes specifies the OAuth scopes available to the user. + // - nil means scopes are not checked (all tools available) + // - empty slice means no scopes (only tools requiring no scopes are available) + // - non-empty slice means only tools whose required scopes are satisfied are available + AvailableScopes []string +} + +// NewToolsetGroup creates a ToolsetGroup from the registry with the given configuration. +// Tools are automatically categorized as read or write based on their ReadOnlyHint annotation. +// Tools are grouped into toolsets based on the "toolset" field in their Meta. +// If AvailableScopes is non-nil, tools are filtered based on scope requirements. +func (r *ToolsetRegistry) NewToolsetGroup(config ToolsetGroupConfig) *ToolsetGroup { + tsg := NewToolsetGroup(config.ReadOnly) + + // Build a map for quick lookup of toolset metadata by ID + metadataByID := make(map[string]ToolsetMetadata) + for _, meta := range r.toolsetMetadatas { + metadataByID[meta.ID] = meta + } + + // Group tools by toolset name, filtering by scopes if specified + toolsByToolset := make(map[string][]ServerTool) + for _, tool := range r.tools { + // Check scope requirements if AvailableScopes is specified + if config.AvailableScopes != nil { + if !toolScopeSatisfied(tool, config.AvailableScopes) { + continue // Skip tools that don't have required scopes + } + } + + toolsetID := getToolsetFromMeta(tool.Tool.Meta) + if toolsetID == "" { + panic(fmt.Sprintf("tool %q has no toolset in Meta", tool.Tool.Name)) + } + toolsByToolset[toolsetID] = append(toolsByToolset[toolsetID], tool) + } + + // Group resources by toolset + resourcesByToolset := make(map[string][]ServerResourceTemplate) + for _, resource := range r.resourceTemplates { + toolsetID := getToolsetFromMeta(resource.Template.Meta) + if toolsetID == "" { + panic(fmt.Sprintf("resource template %q has no toolset in Meta", resource.Template.Name)) + } + resourcesByToolset[toolsetID] = append(resourcesByToolset[toolsetID], resource) + } + + // Group prompts by toolset + promptsByToolset := make(map[string][]ServerPrompt) + for _, prompt := range r.prompts { + toolsetID := getToolsetFromMeta(prompt.Prompt.Meta) + if toolsetID == "" { + panic(fmt.Sprintf("prompt %q has no toolset in Meta", prompt.Prompt.Name)) + } + promptsByToolset[toolsetID] = append(promptsByToolset[toolsetID], prompt) + } + + // Collect all toolset IDs that have tools, resources, or prompts + allToolsetIDs := make(map[string]bool) + for id := range toolsByToolset { + allToolsetIDs[id] = true + } + for id := range resourcesByToolset { + allToolsetIDs[id] = true + } + for id := range promptsByToolset { + allToolsetIDs[id] = true + } + + // Create toolsets and add tools, resources, and prompts + for toolsetID := range allToolsetIDs { + meta, ok := metadataByID[toolsetID] + if !ok { + // Use a default description if not provided + meta = ToolsetMetadata{ID: toolsetID, Description: ""} + } + + ts := NewToolset(meta.ID, meta.Description) + + // Add tools + for _, tool := range toolsByToolset[toolsetID] { + if isReadOnlyTool(tool) { + ts.readTools = append(ts.readTools, tool) + } else { + ts.writeTools = append(ts.writeTools, tool) + } + } + + // Add resources + ts.resourceTemplates = append(ts.resourceTemplates, resourcesByToolset[toolsetID]...) + + // Add prompts + ts.prompts = append(ts.prompts, promptsByToolset[toolsetID]...) + + tsg.AddToolset(ts) + } + + // Enable active toolsets + if len(config.ActiveToolsets) > 0 { + // Ignore errors for non-existent toolsets (they may have been filtered out by scopes) + _ = tsg.EnableToolsets(config.ActiveToolsets, nil) + } + + return tsg +} + +// toolScopeSatisfied checks if the tool's required scopes are satisfied by available scopes. +// Returns true if the tool has no scope requirements or if all requirements are met. +func toolScopeSatisfied(tool ServerTool, availableScopes []string) bool { + if tool.Tool.Meta == nil { + return true // No meta means no scope requirements + } + + requiredScopes, ok := tool.Tool.Meta[scopeMetaKey] + if !ok { + return true // No scope requirements + } + + // Handle both []string and []any (from JSON unmarshaling) + var required []string + switch v := requiredScopes.(type) { + case []string: + required = v + case []any: + required = make([]string, 0, len(v)) + for _, s := range v { + if str, ok := s.(string); ok { + required = append(required, str) + } + } + default: + return true // Unknown format, allow + } + + if len(required) == 0 { + return true // Empty requirements + } + + // Check if any available scope satisfies the requirements + // For now, simple string matching - scope hierarchy should be handled by caller + availableSet := make(map[string]bool) + for _, s := range availableScopes { + availableSet[s] = true + } + + for _, req := range required { + if !availableSet[req] { + return false + } + } + + return true +} + +// scopeMetaKey is the key used to store OAuth scopes in the mcp.Tool.Meta field. +const scopeMetaKey = "requiredOAuthScopes" + +// NewToolsetGroupFromTools creates a ToolsetGroup from a list of ServerTools. +// Tools are automatically categorized as read or write based on their ReadOnlyHint annotation. +// Tools are grouped into toolsets based on the "toolset" field in their Meta. +// The toolsetMetadatas slice provides IDs and descriptions for each toolset. +// +// Deprecated: Use NewToolsetRegistry and ToolsetRegistry.NewToolsetGroup instead. +func NewToolsetGroupFromTools(readOnly bool, toolsetMetadatas []ToolsetMetadata, tools ...ServerTool) *ToolsetGroup { + registry := NewToolsetRegistry(toolsetMetadatas, tools) + return registry.NewToolsetGroup(ToolsetGroupConfig{ + ReadOnly: readOnly, + AvailableScopes: nil, // No scope filtering + }) +} + +// getToolsetFromMeta extracts the toolset name from tool metadata +func getToolsetFromMeta(meta mcp.Meta) string { + if meta == nil { + return "" + } + if toolset, ok := meta["toolset"].(string); ok { + return toolset + } + return "" +} + +// isReadOnlyTool determines if a tool is read-only based on its annotations. +// A tool is considered read-only only if ReadOnlyHint is explicitly true. +// Write tools have ReadOnlyHint=false or DestructiveHint=true. +func isReadOnlyTool(tool ServerTool) bool { + if tool.Tool.Annotations == nil { + // No annotations means we assume it could write (worst case) + return false + } + return tool.Tool.Annotations.ReadOnlyHint +} + func (tg *ToolsetGroup) AddDeprecatedToolAliases(aliases map[string]string) { for oldName, newName := range aliases { tg.deprecatedAliases[oldName] = newName diff --git a/pkg/toolsets/toolsets_test.go b/pkg/toolsets/toolsets_test.go index 6362aad0e..249092054 100644 --- a/pkg/toolsets/toolsets_test.go +++ b/pkg/toolsets/toolsets_test.go @@ -393,3 +393,420 @@ func TestRegisterSpecificTools(t *testing.T) { t.Error("expected error for non-existent tool") } } + +// mockToolWithMeta creates a ServerTool with metadata for testing NewToolsetGroupFromTools +func mockToolWithMeta(name string, toolsetName string, readOnly bool) ServerTool { + return ServerTool{ + Tool: mcp.Tool{ + Name: name, + Meta: mcp.Meta{"toolset": toolsetName}, + Annotations: &mcp.ToolAnnotations{ + ReadOnlyHint: readOnly, + }, + }, + RegisterFunc: func(_ *mcp.Server) {}, + } +} + +// mockToolWithScopes creates a ServerTool with metadata including required scopes +func mockToolWithScopes(name string, toolsetName string, readOnly bool, requiredScopes []string) ServerTool { + meta := mcp.Meta{"toolset": toolsetName} + if requiredScopes != nil { + meta["requiredOAuthScopes"] = requiredScopes + } + return ServerTool{ + Tool: mcp.Tool{ + Name: name, + Meta: meta, + Annotations: &mcp.ToolAnnotations{ + ReadOnlyHint: readOnly, + }, + }, + RegisterFunc: func(_ *mcp.Server) {}, + } +} + +func TestNewToolsetGroupFromTools(t *testing.T) { + toolsetMetadatas := []ToolsetMetadata{ + {ID: "repos", Description: "Repository tools"}, + {ID: "issues", Description: "Issue tools"}, + } + + // Create tools with meta containing toolset info + tools := []ServerTool{ + mockToolWithMeta("get_repo", "repos", true), + mockToolWithMeta("create_repo", "repos", false), + mockToolWithMeta("get_issue", "issues", true), + mockToolWithMeta("create_issue", "issues", false), + mockToolWithMeta("list_issues", "issues", true), + } + + tsg := NewToolsetGroupFromTools(false, toolsetMetadatas, tools...) + + // Verify toolsets were created + if len(tsg.Toolsets) != 2 { + t.Fatalf("expected 2 toolsets, got %d", len(tsg.Toolsets)) + } + + // Verify repos toolset + reposToolset, exists := tsg.Toolsets["repos"] + if !exists { + t.Fatal("expected 'repos' toolset to exist") + } + if reposToolset.Description != "Repository tools" { + t.Errorf("expected repos description 'Repository tools', got '%s'", reposToolset.Description) + } + if len(reposToolset.readTools) != 1 { + t.Errorf("expected 1 read tool in repos, got %d", len(reposToolset.readTools)) + } + if len(reposToolset.writeTools) != 1 { + t.Errorf("expected 1 write tool in repos, got %d", len(reposToolset.writeTools)) + } + + // Verify issues toolset + issuesToolset, exists := tsg.Toolsets["issues"] + if !exists { + t.Fatal("expected 'issues' toolset to exist") + } + if len(issuesToolset.readTools) != 2 { + t.Errorf("expected 2 read tools in issues, got %d", len(issuesToolset.readTools)) + } + if len(issuesToolset.writeTools) != 1 { + t.Errorf("expected 1 write tool in issues, got %d", len(issuesToolset.writeTools)) + } +} + +func TestNewToolsetGroupFromToolsReadOnly(t *testing.T) { + toolsetMetadatas := []ToolsetMetadata{ + {ID: "repos", Description: "Repository tools"}, + } + + tools := []ServerTool{ + mockToolWithMeta("get_repo", "repos", true), + mockToolWithMeta("create_repo", "repos", false), + } + + // Create with readOnly=true + tsg := NewToolsetGroupFromTools(true, toolsetMetadatas, tools...) + + reposToolset := tsg.Toolsets["repos"] + if !reposToolset.readOnly { + t.Error("expected toolset to be in read-only mode") + } + + // GetActiveTools should only return read tools + activeTools := reposToolset.GetActiveTools() + if len(activeTools) != 0 { + // Toolset is not enabled yet + t.Errorf("expected 0 active tools (not enabled), got %d", len(activeTools)) + } + + reposToolset.Enabled = true + activeTools = reposToolset.GetActiveTools() + if len(activeTools) != 1 { + t.Errorf("expected 1 active tool in read-only mode, got %d", len(activeTools)) + } + if activeTools[0].Tool.Name != "get_repo" { + t.Errorf("expected only read tool 'get_repo', got '%s'", activeTools[0].Tool.Name) + } +} + +func TestNewToolsetGroupFromToolsPanicsOnMissingToolset(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Error("expected panic when tool has no toolset in Meta") + } + }() + + // Tool without toolset in meta + tools := []ServerTool{ + { + Tool: mcp.Tool{ + Name: "bad_tool", + Meta: nil, // No meta + Annotations: &mcp.ToolAnnotations{ + ReadOnlyHint: true, + }, + }, + RegisterFunc: func(_ *mcp.Server) {}, + }, + } + + NewToolsetGroupFromTools(false, nil, tools...) +} + +func TestIsReadOnlyTool(t *testing.T) { + tests := []struct { + name string + tool ServerTool + expected bool + }{ + { + name: "read-only tool", + tool: ServerTool{ + Tool: mcp.Tool{ + Annotations: &mcp.ToolAnnotations{ReadOnlyHint: true}, + }, + }, + expected: true, + }, + { + name: "write tool", + tool: ServerTool{ + Tool: mcp.Tool{ + Annotations: &mcp.ToolAnnotations{ReadOnlyHint: false}, + }, + }, + expected: false, + }, + { + name: "no annotations (assume write)", + tool: ServerTool{ + Tool: mcp.Tool{ + Annotations: nil, + }, + }, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := isReadOnlyTool(tt.tool) + if result != tt.expected { + t.Errorf("expected %v, got %v", tt.expected, result) + } + }) + } +} + +func TestGetToolsetFromMeta(t *testing.T) { + tests := []struct { + name string + meta mcp.Meta + expected string + }{ + { + name: "valid toolset", + meta: mcp.Meta{"toolset": "repos"}, + expected: "repos", + }, + { + name: "nil meta", + meta: nil, + expected: "", + }, + { + name: "missing toolset key", + meta: mcp.Meta{"other": "value"}, + expected: "", + }, + { + name: "wrong type for toolset", + meta: mcp.Meta{"toolset": 123}, + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := getToolsetFromMeta(tt.meta) + if result != tt.expected { + t.Errorf("expected %q, got %q", tt.expected, result) + } + }) + } +} + +func TestToolsetRegistry_NewToolsetGroup(t *testing.T) { + toolsetMetadatas := []ToolsetMetadata{ + {ID: "repos", Description: "Repository tools"}, + {ID: "issues", Description: "Issue tools"}, + } + + tools := []ServerTool{ + mockToolWithMeta("get_repo", "repos", true), + mockToolWithMeta("create_repo", "repos", false), + mockToolWithMeta("get_issue", "issues", true), + mockToolWithMeta("create_issue", "issues", false), + } + + registry := NewToolsetRegistry(toolsetMetadatas, tools) + + t.Run("basic creation", func(t *testing.T) { + tsg := registry.NewToolsetGroup(ToolsetGroupConfig{}) + + if len(tsg.Toolsets) != 2 { + t.Fatalf("expected 2 toolsets, got %d", len(tsg.Toolsets)) + } + + // Verify toolsets are not enabled by default + if tsg.IsEnabled("repos") { + t.Error("expected repos to be disabled by default") + } + }) + + t.Run("with active toolsets", func(t *testing.T) { + tsg := registry.NewToolsetGroup(ToolsetGroupConfig{ + ActiveToolsets: []string{"repos"}, + }) + + if !tsg.IsEnabled("repos") { + t.Error("expected repos to be enabled") + } + if tsg.IsEnabled("issues") { + t.Error("expected issues to be disabled") + } + }) + + t.Run("with read-only mode", func(t *testing.T) { + tsg := registry.NewToolsetGroup(ToolsetGroupConfig{ + ReadOnly: true, + ActiveToolsets: []string{"repos"}, + }) + + reposToolset := tsg.Toolsets["repos"] + if !reposToolset.readOnly { + t.Error("expected toolset to be in read-only mode") + } + + activeTools := reposToolset.GetActiveTools() + if len(activeTools) != 1 { + t.Errorf("expected 1 active tool in read-only mode, got %d", len(activeTools)) + } + }) +} + +func TestToolsetRegistry_NewToolsetGroup_WithScopes(t *testing.T) { + toolsetMetadatas := []ToolsetMetadata{ + {ID: "repos", Description: "Repository tools"}, + {ID: "issues", Description: "Issue tools"}, + } + + tools := []ServerTool{ + mockToolWithScopes("get_repo", "repos", true, nil), // No scope required + mockToolWithScopes("create_repo", "repos", false, []string{"repo"}), // Requires repo scope + mockToolWithScopes("get_issue", "issues", true, []string{"repo"}), // Requires repo scope + mockToolWithScopes("public_issue", "issues", true, []string{}), // Empty scopes (no requirement) + } + + registry := NewToolsetRegistry(toolsetMetadatas, tools) + + t.Run("nil scopes allows all tools", func(t *testing.T) { + tsg := registry.NewToolsetGroup(ToolsetGroupConfig{ + AvailableScopes: nil, + }) + + reposToolset := tsg.Toolsets["repos"] + if len(reposToolset.readTools)+len(reposToolset.writeTools) != 2 { + t.Errorf("expected 2 tools in repos, got %d", len(reposToolset.readTools)+len(reposToolset.writeTools)) + } + }) + + t.Run("empty scopes filters tools requiring scopes", func(t *testing.T) { + tsg := registry.NewToolsetGroup(ToolsetGroupConfig{ + AvailableScopes: []string{}, // No scopes available + }) + + // repos should only have get_repo (no scope required) + reposToolset, exists := tsg.Toolsets["repos"] + if !exists { + t.Fatal("expected repos toolset to exist") + } + if len(reposToolset.readTools) != 1 { + t.Errorf("expected 1 read tool in repos (no scope required), got %d", len(reposToolset.readTools)) + } + if len(reposToolset.writeTools) != 0 { + t.Errorf("expected 0 write tools in repos (scope required), got %d", len(reposToolset.writeTools)) + } + + // issues should only have public_issue (empty scopes) + issuesToolset, exists := tsg.Toolsets["issues"] + if !exists { + t.Fatal("expected issues toolset to exist") + } + if len(issuesToolset.readTools) != 1 { + t.Errorf("expected 1 read tool in issues, got %d", len(issuesToolset.readTools)) + } + }) + + t.Run("with repo scope allows repo-scoped tools", func(t *testing.T) { + tsg := registry.NewToolsetGroup(ToolsetGroupConfig{ + AvailableScopes: []string{"repo"}, + }) + + reposToolset := tsg.Toolsets["repos"] + if len(reposToolset.readTools) != 1 { + t.Errorf("expected 1 read tool, got %d", len(reposToolset.readTools)) + } + if len(reposToolset.writeTools) != 1 { + t.Errorf("expected 1 write tool, got %d", len(reposToolset.writeTools)) + } + + issuesToolset := tsg.Toolsets["issues"] + if len(issuesToolset.readTools) != 2 { + t.Errorf("expected 2 read tools in issues, got %d", len(issuesToolset.readTools)) + } + }) +} + +func TestToolScopeSatisfied(t *testing.T) { + tests := []struct { + name string + tool ServerTool + availableScopes []string + expected bool + }{ + { + name: "no meta", + tool: ServerTool{Tool: mcp.Tool{Meta: nil}}, + availableScopes: []string{}, + expected: true, + }, + { + name: "no scope requirement", + tool: mockToolWithScopes("test", "repos", true, nil), + availableScopes: []string{}, + expected: true, + }, + { + name: "empty scope requirement", + tool: mockToolWithScopes("test", "repos", true, []string{}), + availableScopes: []string{}, + expected: true, + }, + { + name: "scope required and available", + tool: mockToolWithScopes("test", "repos", true, []string{"repo"}), + availableScopes: []string{"repo"}, + expected: true, + }, + { + name: "scope required but not available", + tool: mockToolWithScopes("test", "repos", true, []string{"repo"}), + availableScopes: []string{"gist"}, + expected: false, + }, + { + name: "multiple scopes required all available", + tool: mockToolWithScopes("test", "repos", true, []string{"repo", "gist"}), + availableScopes: []string{"repo", "gist", "user"}, + expected: true, + }, + { + name: "multiple scopes required one missing", + tool: mockToolWithScopes("test", "repos", true, []string{"repo", "admin:org"}), + availableScopes: []string{"repo"}, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := toolScopeSatisfied(tt.tool, tt.availableScopes) + if result != tt.expected { + t.Errorf("expected %v, got %v", tt.expected, result) + } + }) + } +}