diff --git a/README.md b/README.md index cf8f62da89..54375c8388 100644 --- a/README.md +++ b/README.md @@ -629,7 +629,7 @@ The issue metadata (`list_issue_types`, `list_issue_fields`), comment (`add_issu - **actions_get** - Get details of GitHub Actions resources (workflows, workflow runs, jobs, and artifacts) - **OAuth Challenge Scopes**: `repo` - - `method`: The method to execute (string, required) + - `method`: The method to execute Supported values: get_workflow, get_workflow_run, get_workflow_job, download_workflow_run_artifact, get_workflow_run_usage, get_workflow_run_logs_url. (string, required) - `owner`: Repository owner (string, required) - `repo`: Repository name (string, required) - `resource_id`: The unique identifier of the resource. This will vary based on the "method" provided, so ensure you provide the correct ID: @@ -641,7 +641,7 @@ The issue metadata (`list_issue_types`, `list_issue_fields`), comment (`add_issu - **actions_list** - List GitHub Actions workflows in a repository - **OAuth Challenge Scopes**: `repo` - - `method`: The action to perform (string, required) + - `method`: The action to perform Supported values: list_workflows, list_workflow_runs, list_workflow_jobs, list_workflow_run_artifacts. (string, required) - `owner`: Repository owner (string, required) - `page`: Page number for pagination (default: 1) (number, optional) - `perPage`: Results per page for pagination (default: 30, max: 100) (number, optional) @@ -651,13 +651,13 @@ The issue metadata (`list_issue_types`, `list_issue_fields`), comment (`add_issu - Provide a workflow ID or workflow file name (e.g. ci.yaml) for 'list_workflow_runs' method, or omit to list all workflow runs in the repository. - Provide a workflow run ID for 'list_workflow_jobs' and 'list_workflow_run_artifacts' methods. (string, optional) - - `workflow_jobs_filter`: Filters for workflow jobs. **ONLY** used when method is 'list_workflow_jobs' (object, optional) - - `workflow_runs_filter`: Filters for workflow runs. **ONLY** used when method is 'list_workflow_runs' (object, optional) + - `workflow_jobs_filter`: Filters for workflow jobs. **ONLY** used when method is 'list_workflow_jobs' (object | any, optional) + - `workflow_runs_filter`: Filters for workflow runs. **ONLY** used when method is 'list_workflow_runs' (object | any, optional) - **actions_run_trigger** - Trigger GitHub Actions workflow actions - **OAuth Challenge Scopes**: `repo` - `inputs`: Inputs the workflow accepts. Only used for 'run_workflow' method. (object, optional) - - `method`: The method to execute (string, required) + - `method`: The method to execute Supported values: run_workflow, rerun_workflow_run, rerun_failed_jobs, cancel_workflow_run, delete_workflow_run_logs. (string, required) - `owner`: Repository owner (string, required) - `ref`: The git reference for the workflow. The reference can be a branch or tag name. Required for 'run_workflow' method. (string, optional) - `repo`: Repository name (string, required) diff --git a/pkg/github/__toolsnaps__/actions_get.snap b/pkg/github/__toolsnaps__/actions_get.snap index 661f379f5f..7424ec26c1 100644 --- a/pkg/github/__toolsnaps__/actions_get.snap +++ b/pkg/github/__toolsnaps__/actions_get.snap @@ -8,15 +8,7 @@ "inputSchema": { "properties": { "method": { - "description": "The method to execute", - "enum": [ - "get_workflow", - "get_workflow_run", - "get_workflow_job", - "download_workflow_run_artifact", - "get_workflow_run_usage", - "get_workflow_run_logs_url" - ], + "description": "The method to execute Supported values: get_workflow, get_workflow_run, get_workflow_job, download_workflow_run_artifact, get_workflow_run_usage, get_workflow_run_logs_url.", "type": "string" }, "owner": { @@ -40,5 +32,686 @@ ], "type": "object" }, - "name": "actions_get" + "name": "actions_get", + "outputSchema": { + "anyOf": [ + { + "type": "null" + }, + { + "additionalProperties": false, + "properties": { + "badge_url": { + "type": [ + "null", + "string" + ] + }, + "created_at": { + "format": "date-time", + "type": [ + "null", + "string" + ] + }, + "html_url": { + "type": [ + "null", + "string" + ] + }, + "id": { + "type": [ + "null", + "integer" + ] + }, + "name": { + "type": [ + "null", + "string" + ] + }, + "node_id": { + "type": [ + "null", + "string" + ] + }, + "path": { + "type": [ + "null", + "string" + ] + }, + "state": { + "type": [ + "null", + "string" + ] + }, + "updated_at": { + "format": "date-time", + "type": [ + "null", + "string" + ] + }, + "url": { + "type": [ + "null", + "string" + ] + } + }, + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "actor": { + "additionalProperties": false, + "properties": { + "avatar_url": { + "type": "string" + }, + "details": { + "additionalProperties": false, + "properties": { + "bio": { + "type": "string" + }, + "blog": { + "type": "string" + }, + "company": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "email": { + "type": "string" + }, + "followers": { + "type": "integer" + }, + "following": { + "type": "integer" + }, + "hireable": { + "type": "boolean" + }, + "location": { + "type": "string" + }, + "name": { + "type": "string" + }, + "owned_private_repos": { + "type": "integer" + }, + "private_gists": { + "type": "integer" + }, + "public_gists": { + "type": "integer" + }, + "public_repos": { + "type": "integer" + }, + "total_private_repos": { + "type": "integer" + }, + "twitter_username": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + }, + "required": [ + "public_repos", + "public_gists", + "followers", + "following", + "created_at", + "updated_at" + ], + "type": [ + "null", + "object" + ] + }, + "id": { + "type": "integer" + }, + "login": { + "type": "string" + }, + "profile_url": { + "type": "string" + } + }, + "required": [ + "login" + ], + "type": [ + "null", + "object" + ] + }, + "conclusion": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "display_title": { + "type": "string" + }, + "event": { + "type": "string" + }, + "head_branch": { + "type": "string" + }, + "head_commit": { + "additionalProperties": false, + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": [ + "null", + "object" + ] + }, + "head_sha": { + "type": "string" + }, + "html_url": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "pull_requests": { + "items": { + "type": "integer" + }, + "type": [ + "null", + "array" + ] + }, + "referenced_workflows": { + "items": { + "additionalProperties": false, + "properties": { + "path": { + "type": "string" + }, + "ref": { + "type": "string" + }, + "sha": { + "type": "string" + } + }, + "type": "object" + }, + "type": [ + "null", + "array" + ] + }, + "run_attempt": { + "type": "integer" + }, + "run_number": { + "type": "integer" + }, + "run_started_at": { + "type": "string" + }, + "status": { + "type": "string" + }, + "triggering_actor": { + "additionalProperties": false, + "properties": { + "avatar_url": { + "type": "string" + }, + "details": { + "additionalProperties": false, + "properties": { + "bio": { + "type": "string" + }, + "blog": { + "type": "string" + }, + "company": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "email": { + "type": "string" + }, + "followers": { + "type": "integer" + }, + "following": { + "type": "integer" + }, + "hireable": { + "type": "boolean" + }, + "location": { + "type": "string" + }, + "name": { + "type": "string" + }, + "owned_private_repos": { + "type": "integer" + }, + "private_gists": { + "type": "integer" + }, + "public_gists": { + "type": "integer" + }, + "public_repos": { + "type": "integer" + }, + "total_private_repos": { + "type": "integer" + }, + "twitter_username": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + }, + "required": [ + "public_repos", + "public_gists", + "followers", + "following", + "created_at", + "updated_at" + ], + "type": [ + "null", + "object" + ] + }, + "id": { + "type": "integer" + }, + "login": { + "type": "string" + }, + "profile_url": { + "type": "string" + } + }, + "required": [ + "login" + ], + "type": [ + "null", + "object" + ] + }, + "updated_at": { + "type": "string" + }, + "workflow_id": { + "type": "integer" + } + }, + "required": [ + "id", + "name", + "workflow_id", + "run_number", + "run_attempt", + "status" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "check_run_url": { + "type": [ + "null", + "string" + ] + }, + "completed_at": { + "format": "date-time", + "type": [ + "null", + "string" + ] + }, + "conclusion": { + "type": [ + "null", + "string" + ] + }, + "created_at": { + "format": "date-time", + "type": [ + "null", + "string" + ] + }, + "head_branch": { + "type": [ + "null", + "string" + ] + }, + "head_sha": { + "type": [ + "null", + "string" + ] + }, + "html_url": { + "type": [ + "null", + "string" + ] + }, + "id": { + "type": [ + "null", + "integer" + ] + }, + "labels": { + "items": { + "type": "string" + }, + "type": [ + "null", + "array" + ] + }, + "name": { + "type": [ + "null", + "string" + ] + }, + "node_id": { + "type": [ + "null", + "string" + ] + }, + "run_attempt": { + "type": [ + "null", + "integer" + ] + }, + "run_id": { + "type": [ + "null", + "integer" + ] + }, + "run_url": { + "type": [ + "null", + "string" + ] + }, + "runner_group_id": { + "type": [ + "null", + "integer" + ] + }, + "runner_group_name": { + "type": [ + "null", + "string" + ] + }, + "runner_id": { + "type": [ + "null", + "integer" + ] + }, + "runner_name": { + "type": [ + "null", + "string" + ] + }, + "started_at": { + "format": "date-time", + "type": [ + "null", + "string" + ] + }, + "status": { + "type": [ + "null", + "string" + ] + }, + "steps": { + "items": { + "additionalProperties": false, + "properties": { + "completed_at": { + "format": "date-time", + "type": [ + "null", + "string" + ] + }, + "conclusion": { + "type": [ + "null", + "string" + ] + }, + "name": { + "type": [ + "null", + "string" + ] + }, + "number": { + "type": [ + "null", + "integer" + ] + }, + "started_at": { + "format": "date-time", + "type": [ + "null", + "string" + ] + }, + "status": { + "type": [ + "null", + "string" + ] + } + }, + "type": [ + "null", + "object" + ] + }, + "type": [ + "null", + "array" + ] + }, + "url": { + "type": [ + "null", + "string" + ] + }, + "workflow_name": { + "type": [ + "null", + "string" + ] + } + }, + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "billable": { + "additionalProperties": { + "additionalProperties": false, + "properties": { + "job_runs": { + "items": { + "additionalProperties": false, + "properties": { + "duration_ms": { + "type": [ + "null", + "integer" + ] + }, + "job_id": { + "type": [ + "null", + "integer" + ] + } + }, + "type": [ + "null", + "object" + ] + }, + "type": [ + "null", + "array" + ] + }, + "jobs": { + "type": [ + "null", + "integer" + ] + }, + "total_ms": { + "type": [ + "null", + "integer" + ] + } + }, + "type": [ + "null", + "object" + ] + }, + "type": [ + "null", + "object" + ] + }, + "run_duration_ms": { + "type": [ + "null", + "integer" + ] + } + }, + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "artifact_id": { + "type": "integer" + }, + "download_url": { + "type": "string" + }, + "message": { + "type": "string" + }, + "note": { + "type": "string" + } + }, + "required": [ + "artifact_id", + "download_url", + "message", + "note" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "logs_url": { + "type": "string" + }, + "message": { + "type": "string" + }, + "note": { + "type": "string" + }, + "optimization_tip": { + "type": "string" + }, + "warning": { + "type": "string" + } + }, + "required": [ + "logs_url", + "message", + "note", + "optimization_tip", + "warning" + ], + "type": "object" + } + ] + } } \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/actions_get_typed.snap b/pkg/github/__toolsnaps__/actions_get_typed.snap new file mode 100644 index 0000000000..69c9574e57 --- /dev/null +++ b/pkg/github/__toolsnaps__/actions_get_typed.snap @@ -0,0 +1,731 @@ +{ + "annotations": { + "idempotentHint": false, + "readOnlyHint": true, + "title": "Get details of GitHub Actions resources (workflows, workflow runs, jobs, and artifacts)" + }, + "description": "Get details about specific GitHub Actions resources.\nUse this tool to get details about individual workflows, workflow runs, jobs, and artifacts by their unique IDs.\n", + "icons": [ + { + "mimeType": "image/png", + "src": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAABqUlEQVRIie2UMUtbURTHf+fmqdQKBR21k8+SF9AXDHTRyaWBbqWddLZCyWcwawc7ONUhW2YHBeNX6BSDxEB57ZJJkICgAZPHOx0iVfC+0hvJIHiWyz3nf87vcvnfCyMOAVgI8u8V3QNmU3Q9VA6IrzaiKLpxAXgAqvodoQNUbSKFVyK6xdjkMVBxBiDMIVKJzk7KaUI/CLcQ89plOIBxbXgGPF1ATxKdThNls9kZAJLE6Q3AnU0PFUp+EJZsoniw9E0mczQcoHe9zvjLI5Q3t8BVlBXg62DLlRhz+LNZb7gCxJb0c/kyqttRq2GvB0vLKqyJSsY+Vi/od6tRFN14rifyg6VlkB+i/KNXwJsqAp+cASjvELwJE880m82OTbKQDXdV9DOk2FSVPkChUBh7UDRmAiBtOIAa6QDj6QCoA1xexyUrxCGsV/S7dVKbD8J9EXYuu/GOH4T36Pp4AKC/Wo2P87nFoiTyFpEXfyt3Fn4UYAA5O60BtftJP5cvg/43YOR/0RA2Tdog+EH4DTi3a/QD0B4O0O9W8aaKiH7h1oqWaKvRTefZw8QfOA2GtnSwHcIAAAAASUVORK5CYII=", + "theme": "light" + }, + { + "mimeType": "image/png", + "src": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAABLElEQVRIie2Wr04DQRjEf8O/C4qEvgOWBIEpCkMTEgQBhUVgeAbOYjDoOhygSHgHFOEVKIaEFIO5IgbBXlJxu2WvKQkJY+7PNzszm/2+y8FvwPau7RfHUdm+tl3kaisYDIB34D7CWwFOgGNJ/TY7sO1yWk4T5rLT/Bv8WYMRsBoj2e6E26qVi+2bxJDVGNlez9WuB60AjoC18H4L6ALn4fkDuJP01MqgYUclcCYpVt8AtoH5iO4bcCWpWshNFMQfgElre8Bhmy7aCeIdRQBcAnsQb9PPkHaxoVYASBomQgyBpZTBY7ieRkymg23Zvk317IT1Zc1pPChJtn3A90FtAstj5bqFZ4PxdD/hzPxblD0HwADA9gXwGuHs17xs2C7CD0CV6IFn271WBrn4Aj4U/yN7l7QYAAAAAElFTkSuQmCC", + "theme": "dark" + } + ], + "inputSchema": { + "properties": { + "method": { + "description": "The method to execute Supported values: get_workflow, get_workflow_run, get_workflow_job, download_workflow_run_artifact, get_workflow_run_usage, get_workflow_run_logs_url.", + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string", + "x-mcp-header": "owner" + }, + "repo": { + "description": "Repository name", + "type": "string", + "x-mcp-header": "repo" + }, + "resource_id": { + "description": "The unique identifier of the resource. This will vary based on the \"method\" provided, so ensure you provide the correct ID:\n- Provide a workflow ID or workflow file name (e.g. ci.yaml) for 'get_workflow' method.\n- Provide a workflow run ID for 'get_workflow_run', 'get_workflow_run_usage', and 'get_workflow_run_logs_url' methods.\n- Provide an artifact ID for 'download_workflow_run_artifact' method.\n- Provide a job ID for 'get_workflow_job' method.\n", + "type": "string" + } + }, + "required": [ + "method", + "owner", + "repo", + "resource_id" + ], + "type": "object" + }, + "name": "actions_get", + "outputSchema": { + "anyOf": [ + { + "type": "null" + }, + { + "additionalProperties": false, + "properties": { + "badge_url": { + "type": [ + "null", + "string" + ] + }, + "created_at": { + "format": "date-time", + "type": [ + "null", + "string" + ] + }, + "html_url": { + "type": [ + "null", + "string" + ] + }, + "id": { + "type": [ + "null", + "integer" + ] + }, + "name": { + "type": [ + "null", + "string" + ] + }, + "node_id": { + "type": [ + "null", + "string" + ] + }, + "path": { + "type": [ + "null", + "string" + ] + }, + "state": { + "type": [ + "null", + "string" + ] + }, + "updated_at": { + "format": "date-time", + "type": [ + "null", + "string" + ] + }, + "url": { + "type": [ + "null", + "string" + ] + } + }, + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "actor": { + "additionalProperties": false, + "properties": { + "avatar_url": { + "type": "string" + }, + "details": { + "additionalProperties": false, + "properties": { + "bio": { + "type": "string" + }, + "blog": { + "type": "string" + }, + "company": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "email": { + "type": "string" + }, + "followers": { + "type": "integer" + }, + "following": { + "type": "integer" + }, + "hireable": { + "type": "boolean" + }, + "location": { + "type": "string" + }, + "name": { + "type": "string" + }, + "owned_private_repos": { + "type": "integer" + }, + "private_gists": { + "type": "integer" + }, + "public_gists": { + "type": "integer" + }, + "public_repos": { + "type": "integer" + }, + "total_private_repos": { + "type": "integer" + }, + "twitter_username": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + }, + "required": [ + "public_repos", + "public_gists", + "followers", + "following", + "created_at", + "updated_at" + ], + "type": [ + "null", + "object" + ] + }, + "id": { + "type": "integer" + }, + "login": { + "type": "string" + }, + "profile_url": { + "type": "string" + } + }, + "required": [ + "login" + ], + "type": [ + "null", + "object" + ] + }, + "conclusion": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "display_title": { + "type": "string" + }, + "event": { + "type": "string" + }, + "head_branch": { + "type": "string" + }, + "head_commit": { + "additionalProperties": false, + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": [ + "null", + "object" + ] + }, + "head_sha": { + "type": "string" + }, + "html_url": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "pull_requests": { + "items": { + "type": "integer" + }, + "type": [ + "null", + "array" + ] + }, + "referenced_workflows": { + "items": { + "additionalProperties": false, + "properties": { + "path": { + "type": "string" + }, + "ref": { + "type": "string" + }, + "sha": { + "type": "string" + } + }, + "type": "object" + }, + "type": [ + "null", + "array" + ] + }, + "run_attempt": { + "type": "integer" + }, + "run_number": { + "type": "integer" + }, + "run_started_at": { + "type": "string" + }, + "status": { + "type": "string" + }, + "triggering_actor": { + "additionalProperties": false, + "properties": { + "avatar_url": { + "type": "string" + }, + "details": { + "additionalProperties": false, + "properties": { + "bio": { + "type": "string" + }, + "blog": { + "type": "string" + }, + "company": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "email": { + "type": "string" + }, + "followers": { + "type": "integer" + }, + "following": { + "type": "integer" + }, + "hireable": { + "type": "boolean" + }, + "location": { + "type": "string" + }, + "name": { + "type": "string" + }, + "owned_private_repos": { + "type": "integer" + }, + "private_gists": { + "type": "integer" + }, + "public_gists": { + "type": "integer" + }, + "public_repos": { + "type": "integer" + }, + "total_private_repos": { + "type": "integer" + }, + "twitter_username": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + }, + "required": [ + "public_repos", + "public_gists", + "followers", + "following", + "created_at", + "updated_at" + ], + "type": [ + "null", + "object" + ] + }, + "id": { + "type": "integer" + }, + "login": { + "type": "string" + }, + "profile_url": { + "type": "string" + } + }, + "required": [ + "login" + ], + "type": [ + "null", + "object" + ] + }, + "updated_at": { + "type": "string" + }, + "workflow_id": { + "type": "integer" + } + }, + "required": [ + "id", + "name", + "workflow_id", + "run_number", + "run_attempt", + "status" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "check_run_url": { + "type": [ + "null", + "string" + ] + }, + "completed_at": { + "format": "date-time", + "type": [ + "null", + "string" + ] + }, + "conclusion": { + "type": [ + "null", + "string" + ] + }, + "created_at": { + "format": "date-time", + "type": [ + "null", + "string" + ] + }, + "head_branch": { + "type": [ + "null", + "string" + ] + }, + "head_sha": { + "type": [ + "null", + "string" + ] + }, + "html_url": { + "type": [ + "null", + "string" + ] + }, + "id": { + "type": [ + "null", + "integer" + ] + }, + "labels": { + "items": { + "type": "string" + }, + "type": [ + "null", + "array" + ] + }, + "name": { + "type": [ + "null", + "string" + ] + }, + "node_id": { + "type": [ + "null", + "string" + ] + }, + "run_attempt": { + "type": [ + "null", + "integer" + ] + }, + "run_id": { + "type": [ + "null", + "integer" + ] + }, + "run_url": { + "type": [ + "null", + "string" + ] + }, + "runner_group_id": { + "type": [ + "null", + "integer" + ] + }, + "runner_group_name": { + "type": [ + "null", + "string" + ] + }, + "runner_id": { + "type": [ + "null", + "integer" + ] + }, + "runner_name": { + "type": [ + "null", + "string" + ] + }, + "started_at": { + "format": "date-time", + "type": [ + "null", + "string" + ] + }, + "status": { + "type": [ + "null", + "string" + ] + }, + "steps": { + "items": { + "additionalProperties": false, + "properties": { + "completed_at": { + "format": "date-time", + "type": [ + "null", + "string" + ] + }, + "conclusion": { + "type": [ + "null", + "string" + ] + }, + "name": { + "type": [ + "null", + "string" + ] + }, + "number": { + "type": [ + "null", + "integer" + ] + }, + "started_at": { + "format": "date-time", + "type": [ + "null", + "string" + ] + }, + "status": { + "type": [ + "null", + "string" + ] + } + }, + "type": [ + "null", + "object" + ] + }, + "type": [ + "null", + "array" + ] + }, + "url": { + "type": [ + "null", + "string" + ] + }, + "workflow_name": { + "type": [ + "null", + "string" + ] + } + }, + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "billable": { + "additionalProperties": { + "additionalProperties": false, + "properties": { + "job_runs": { + "items": { + "additionalProperties": false, + "properties": { + "duration_ms": { + "type": [ + "null", + "integer" + ] + }, + "job_id": { + "type": [ + "null", + "integer" + ] + } + }, + "type": [ + "null", + "object" + ] + }, + "type": [ + "null", + "array" + ] + }, + "jobs": { + "type": [ + "null", + "integer" + ] + }, + "total_ms": { + "type": [ + "null", + "integer" + ] + } + }, + "type": [ + "null", + "object" + ] + }, + "type": [ + "null", + "object" + ] + }, + "run_duration_ms": { + "type": [ + "null", + "integer" + ] + } + }, + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "artifact_id": { + "type": "integer" + }, + "download_url": { + "type": "string" + }, + "message": { + "type": "string" + }, + "note": { + "type": "string" + } + }, + "required": [ + "artifact_id", + "download_url", + "message", + "note" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "logs_url": { + "type": "string" + }, + "message": { + "type": "string" + }, + "note": { + "type": "string" + }, + "optimization_tip": { + "type": "string" + }, + "warning": { + "type": "string" + } + }, + "required": [ + "logs_url", + "message", + "note", + "optimization_tip", + "warning" + ], + "type": "object" + } + ] + } +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/actions_list.snap b/pkg/github/__toolsnaps__/actions_list.snap index 4df628bf85..6dba51a421 100644 --- a/pkg/github/__toolsnaps__/actions_list.snap +++ b/pkg/github/__toolsnaps__/actions_list.snap @@ -8,13 +8,7 @@ "inputSchema": { "properties": { "method": { - "description": "The action to perform", - "enum": [ - "list_workflows", - "list_workflow_runs", - "list_workflow_jobs", - "list_workflow_run_artifacts" - ], + "description": "The action to perform Supported values: list_workflows, list_workflow_runs, list_workflow_jobs, list_workflow_run_artifacts.", "type": "string" }, "owner": { @@ -41,81 +35,56 @@ "type": "string" }, "workflow_jobs_filter": { - "description": "Filters for workflow jobs. **ONLY** used when method is 'list_workflow_jobs'", - "properties": { - "filter": { - "description": "Filters jobs by their completed_at timestamp", - "enum": [ - "latest", - "all" - ], - "type": "string" + "anyOf": [ + { + "description": "Filters for workflow jobs. **ONLY** used when method is 'list_workflow_jobs'", + "properties": { + "filter": { + "description": "Filters jobs by their completed_at timestamp Supported values: latest, all.", + "type": "string" + } + }, + "type": "object" + }, + { + "not": { + "type": "object" + } } - }, - "type": "object" + ], + "description": "Filters for workflow jobs. **ONLY** used when method is 'list_workflow_jobs'" }, "workflow_runs_filter": { - "description": "Filters for workflow runs. **ONLY** used when method is 'list_workflow_runs'", - "properties": { - "actor": { - "description": "Filter to a specific GitHub user's workflow runs.", - "type": "string" - }, - "branch": { - "description": "Filter workflow runs to a specific Git branch. Use the name of the branch.", - "type": "string" - }, - "event": { - "description": "Filter workflow runs to a specific event type", - "enum": [ - "branch_protection_rule", - "check_run", - "check_suite", - "create", - "delete", - "deployment", - "deployment_status", - "discussion", - "discussion_comment", - "fork", - "gollum", - "issue_comment", - "issues", - "label", - "merge_group", - "milestone", - "page_build", - "public", - "pull_request", - "pull_request_review", - "pull_request_review_comment", - "pull_request_target", - "push", - "registry_package", - "release", - "repository_dispatch", - "schedule", - "status", - "watch", - "workflow_call", - "workflow_dispatch", - "workflow_run" - ], - "type": "string" + "anyOf": [ + { + "description": "Filters for workflow runs. **ONLY** used when method is 'list_workflow_runs'", + "properties": { + "actor": { + "description": "Filter to a specific GitHub user's workflow runs.", + "type": "string" + }, + "branch": { + "description": "Filter workflow runs to a specific Git branch. Use the name of the branch.", + "type": "string" + }, + "event": { + "description": "Filter workflow runs to a specific event type Supported values: branch_protection_rule, check_run, check_suite, create, delete, deployment, deployment_status, discussion, discussion_comment, fork, gollum, issue_comment, issues, label, merge_group, milestone, page_build, public, pull_request, pull_request_review, pull_request_review_comment, pull_request_target, push, registry_package, release, repository_dispatch, schedule, status, watch, workflow_call, workflow_dispatch, workflow_run.", + "type": "string" + }, + "status": { + "description": "Filter workflow runs to only runs with a specific status Supported values: queued, in_progress, completed, requested, waiting.", + "type": "string" + } + }, + "type": "object" }, - "status": { - "description": "Filter workflow runs to only runs with a specific status", - "enum": [ - "queued", - "in_progress", - "completed", - "requested", - "waiting" - ], - "type": "string" + { + "not": { + "type": "object" + } } - }, - "type": "object" + ], + "description": "Filters for workflow runs. **ONLY** used when method is 'list_workflow_runs'" } }, "required": [ @@ -125,5 +94,681 @@ ], "type": "object" }, - "name": "actions_list" + "name": "actions_list", + "outputSchema": { + "anyOf": [ + { + "type": "null" + }, + { + "additionalProperties": false, + "properties": { + "total_count": { + "type": [ + "null", + "integer" + ] + }, + "workflows": { + "items": { + "additionalProperties": false, + "properties": { + "badge_url": { + "type": [ + "null", + "string" + ] + }, + "created_at": { + "format": "date-time", + "type": [ + "null", + "string" + ] + }, + "html_url": { + "type": [ + "null", + "string" + ] + }, + "id": { + "type": [ + "null", + "integer" + ] + }, + "name": { + "type": [ + "null", + "string" + ] + }, + "node_id": { + "type": [ + "null", + "string" + ] + }, + "path": { + "type": [ + "null", + "string" + ] + }, + "state": { + "type": [ + "null", + "string" + ] + }, + "updated_at": { + "format": "date-time", + "type": [ + "null", + "string" + ] + }, + "url": { + "type": [ + "null", + "string" + ] + } + }, + "type": [ + "null", + "object" + ] + }, + "type": [ + "null", + "array" + ] + } + }, + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "total_count": { + "type": "integer" + }, + "workflow_runs": { + "items": { + "additionalProperties": false, + "properties": { + "actor": { + "additionalProperties": false, + "properties": { + "avatar_url": { + "type": "string" + }, + "details": { + "additionalProperties": false, + "properties": { + "bio": { + "type": "string" + }, + "blog": { + "type": "string" + }, + "company": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "email": { + "type": "string" + }, + "followers": { + "type": "integer" + }, + "following": { + "type": "integer" + }, + "hireable": { + "type": "boolean" + }, + "location": { + "type": "string" + }, + "name": { + "type": "string" + }, + "owned_private_repos": { + "type": "integer" + }, + "private_gists": { + "type": "integer" + }, + "public_gists": { + "type": "integer" + }, + "public_repos": { + "type": "integer" + }, + "total_private_repos": { + "type": "integer" + }, + "twitter_username": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + }, + "required": [ + "public_repos", + "public_gists", + "followers", + "following", + "created_at", + "updated_at" + ], + "type": [ + "null", + "object" + ] + }, + "id": { + "type": "integer" + }, + "login": { + "type": "string" + }, + "profile_url": { + "type": "string" + } + }, + "required": [ + "login" + ], + "type": [ + "null", + "object" + ] + }, + "conclusion": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "display_title": { + "type": "string" + }, + "event": { + "type": "string" + }, + "head_branch": { + "type": "string" + }, + "head_commit": { + "additionalProperties": false, + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": [ + "null", + "object" + ] + }, + "head_sha": { + "type": "string" + }, + "html_url": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "pull_requests": { + "items": { + "type": "integer" + }, + "type": [ + "null", + "array" + ] + }, + "referenced_workflows": { + "items": { + "additionalProperties": false, + "properties": { + "path": { + "type": "string" + }, + "ref": { + "type": "string" + }, + "sha": { + "type": "string" + } + }, + "type": "object" + }, + "type": [ + "null", + "array" + ] + }, + "run_attempt": { + "type": "integer" + }, + "run_number": { + "type": "integer" + }, + "run_started_at": { + "type": "string" + }, + "status": { + "type": "string" + }, + "triggering_actor": { + "additionalProperties": false, + "properties": { + "avatar_url": { + "type": "string" + }, + "details": { + "additionalProperties": false, + "properties": { + "bio": { + "type": "string" + }, + "blog": { + "type": "string" + }, + "company": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "email": { + "type": "string" + }, + "followers": { + "type": "integer" + }, + "following": { + "type": "integer" + }, + "hireable": { + "type": "boolean" + }, + "location": { + "type": "string" + }, + "name": { + "type": "string" + }, + "owned_private_repos": { + "type": "integer" + }, + "private_gists": { + "type": "integer" + }, + "public_gists": { + "type": "integer" + }, + "public_repos": { + "type": "integer" + }, + "total_private_repos": { + "type": "integer" + }, + "twitter_username": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + }, + "required": [ + "public_repos", + "public_gists", + "followers", + "following", + "created_at", + "updated_at" + ], + "type": [ + "null", + "object" + ] + }, + "id": { + "type": "integer" + }, + "login": { + "type": "string" + }, + "profile_url": { + "type": "string" + } + }, + "required": [ + "login" + ], + "type": [ + "null", + "object" + ] + }, + "updated_at": { + "type": "string" + }, + "workflow_id": { + "type": "integer" + } + }, + "required": [ + "id", + "name", + "workflow_id", + "run_number", + "run_attempt", + "status" + ], + "type": "object" + }, + "type": [ + "null", + "array" + ] + } + }, + "required": [ + "total_count", + "workflow_runs" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "jobs": { + "additionalProperties": false, + "properties": { + "jobs": { + "items": { + "additionalProperties": false, + "properties": { + "completed_at": { + "type": "string" + }, + "conclusion": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "head_branch": { + "type": "string" + }, + "head_sha": { + "type": "string" + }, + "html_url": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "labels": { + "items": { + "type": "string" + }, + "type": [ + "null", + "array" + ] + }, + "name": { + "type": "string" + }, + "run_attempt": { + "type": "integer" + }, + "run_id": { + "type": "integer" + }, + "runner_group_id": { + "type": "integer" + }, + "runner_group_name": { + "type": "string" + }, + "runner_id": { + "type": "integer" + }, + "runner_name": { + "type": "string" + }, + "started_at": { + "type": "string" + }, + "status": { + "type": "string" + }, + "steps": { + "items": { + "additionalProperties": false, + "properties": { + "completed_at": { + "type": "string" + }, + "conclusion": { + "type": "string" + }, + "name": { + "type": "string" + }, + "number": { + "type": "integer" + }, + "started_at": { + "type": "string" + }, + "status": { + "type": "string" + } + }, + "required": [ + "name", + "status", + "number" + ], + "type": "object" + }, + "type": [ + "null", + "array" + ] + }, + "workflow_name": { + "type": "string" + } + }, + "required": [ + "id", + "run_id", + "name", + "status" + ], + "type": "object" + }, + "type": [ + "null", + "array" + ] + }, + "total_count": { + "type": "integer" + } + }, + "required": [ + "total_count", + "jobs" + ], + "type": "object" + } + }, + "required": [ + "jobs" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "artifacts": { + "items": { + "additionalProperties": false, + "properties": { + "archive_download_url": { + "type": [ + "null", + "string" + ] + }, + "created_at": { + "format": "date-time", + "type": [ + "null", + "string" + ] + }, + "digest": { + "type": [ + "null", + "string" + ] + }, + "expired": { + "type": [ + "null", + "boolean" + ] + }, + "expires_at": { + "format": "date-time", + "type": [ + "null", + "string" + ] + }, + "id": { + "type": [ + "null", + "integer" + ] + }, + "name": { + "type": [ + "null", + "string" + ] + }, + "node_id": { + "type": [ + "null", + "string" + ] + }, + "size_in_bytes": { + "type": [ + "null", + "integer" + ] + }, + "updated_at": { + "format": "date-time", + "type": [ + "null", + "string" + ] + }, + "url": { + "type": [ + "null", + "string" + ] + }, + "workflow_run": { + "additionalProperties": false, + "properties": { + "head_branch": { + "type": [ + "null", + "string" + ] + }, + "head_repository_id": { + "type": [ + "null", + "integer" + ] + }, + "head_sha": { + "type": [ + "null", + "string" + ] + }, + "id": { + "type": [ + "null", + "integer" + ] + }, + "repository_id": { + "type": [ + "null", + "integer" + ] + } + }, + "type": [ + "null", + "object" + ] + } + }, + "type": [ + "null", + "object" + ] + }, + "type": [ + "null", + "array" + ] + }, + "total_count": { + "type": [ + "null", + "integer" + ] + } + }, + "type": "object" + } + ] + } } \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/actions_list_typed.snap b/pkg/github/__toolsnaps__/actions_list_typed.snap new file mode 100644 index 0000000000..e26a643b5d --- /dev/null +++ b/pkg/github/__toolsnaps__/actions_list_typed.snap @@ -0,0 +1,788 @@ +{ + "annotations": { + "idempotentHint": false, + "readOnlyHint": true, + "title": "List GitHub Actions workflows in a repository" + }, + "description": "Tools for listing GitHub Actions resources.\nUse this tool to list workflows in a repository, or list workflow runs, jobs, and artifacts for a specific workflow or workflow run.\n", + "icons": [ + { + "mimeType": "image/png", + "src": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAABqUlEQVRIie2UMUtbURTHf+fmqdQKBR21k8+SF9AXDHTRyaWBbqWddLZCyWcwawc7ONUhW2YHBeNX6BSDxEB57ZJJkICgAZPHOx0iVfC+0hvJIHiWyz3nf87vcvnfCyMOAVgI8u8V3QNmU3Q9VA6IrzaiKLpxAXgAqvodoQNUbSKFVyK6xdjkMVBxBiDMIVKJzk7KaUI/CLcQ89plOIBxbXgGPF1ATxKdThNls9kZAJLE6Q3AnU0PFUp+EJZsoniw9E0mczQcoHe9zvjLI5Q3t8BVlBXg62DLlRhz+LNZb7gCxJb0c/kyqttRq2GvB0vLKqyJSsY+Vi/od6tRFN14rifyg6VlkB+i/KNXwJsqAp+cASjvELwJE880m82OTbKQDXdV9DOk2FSVPkChUBh7UDRmAiBtOIAa6QDj6QCoA1xexyUrxCGsV/S7dVKbD8J9EXYuu/GOH4T36Pp4AKC/Wo2P87nFoiTyFpEXfyt3Fn4UYAA5O60BtftJP5cvg/43YOR/0RA2Tdog+EH4DTi3a/QD0B4O0O9W8aaKiH7h1oqWaKvRTefZw8QfOA2GtnSwHcIAAAAASUVORK5CYII=", + "theme": "light" + }, + { + "mimeType": "image/png", + "src": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAABLElEQVRIie2Wr04DQRjEf8O/C4qEvgOWBIEpCkMTEgQBhUVgeAbOYjDoOhygSHgHFOEVKIaEFIO5IgbBXlJxu2WvKQkJY+7PNzszm/2+y8FvwPau7RfHUdm+tl3kaisYDIB34D7CWwFOgGNJ/TY7sO1yWk4T5rLT/Bv8WYMRsBoj2e6E26qVi+2bxJDVGNlez9WuB60AjoC18H4L6ALn4fkDuJP01MqgYUclcCYpVt8AtoH5iO4bcCWpWshNFMQfgElre8Bhmy7aCeIdRQBcAnsQb9PPkHaxoVYASBomQgyBpZTBY7ieRkymg23Zvk317IT1Zc1pPChJtn3A90FtAstj5bqFZ4PxdD/hzPxblD0HwADA9gXwGuHs17xs2C7CD0CV6IFn271WBrn4Aj4U/yN7l7QYAAAAAElFTkSuQmCC", + "theme": "dark" + } + ], + "inputSchema": { + "properties": { + "method": { + "description": "The action to perform Supported values: list_workflows, list_workflow_runs, list_workflow_jobs, list_workflow_run_artifacts.", + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string", + "x-mcp-header": "owner" + }, + "page": { + "description": "Page number for pagination (default: 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (default: 30, max: 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string", + "x-mcp-header": "repo" + }, + "resource_id": { + "description": "The unique identifier of the resource. This will vary based on the \"method\" provided, so ensure you provide the correct ID:\n- Do not provide any resource ID for 'list_workflows' method.\n- Provide a workflow ID or workflow file name (e.g. ci.yaml) for 'list_workflow_runs' method, or omit to list all workflow runs in the repository.\n- Provide a workflow run ID for 'list_workflow_jobs' and 'list_workflow_run_artifacts' methods.\n", + "type": "string" + }, + "workflow_jobs_filter": { + "anyOf": [ + { + "description": "Filters for workflow jobs. **ONLY** used when method is 'list_workflow_jobs'", + "properties": { + "filter": { + "description": "Filters jobs by their completed_at timestamp Supported values: latest, all.", + "type": "string" + } + }, + "type": "object" + }, + { + "not": { + "type": "object" + } + } + ], + "description": "Filters for workflow jobs. **ONLY** used when method is 'list_workflow_jobs'" + }, + "workflow_runs_filter": { + "anyOf": [ + { + "description": "Filters for workflow runs. **ONLY** used when method is 'list_workflow_runs'", + "properties": { + "actor": { + "description": "Filter to a specific GitHub user's workflow runs.", + "type": "string" + }, + "branch": { + "description": "Filter workflow runs to a specific Git branch. Use the name of the branch.", + "type": "string" + }, + "event": { + "description": "Filter workflow runs to a specific event type Supported values: branch_protection_rule, check_run, check_suite, create, delete, deployment, deployment_status, discussion, discussion_comment, fork, gollum, issue_comment, issues, label, merge_group, milestone, page_build, public, pull_request, pull_request_review, pull_request_review_comment, pull_request_target, push, registry_package, release, repository_dispatch, schedule, status, watch, workflow_call, workflow_dispatch, workflow_run.", + "type": "string" + }, + "status": { + "description": "Filter workflow runs to only runs with a specific status Supported values: queued, in_progress, completed, requested, waiting.", + "type": "string" + } + }, + "type": "object" + }, + { + "not": { + "type": "object" + } + } + ], + "description": "Filters for workflow runs. **ONLY** used when method is 'list_workflow_runs'" + } + }, + "required": [ + "method", + "owner", + "repo" + ], + "type": "object" + }, + "name": "actions_list", + "outputSchema": { + "anyOf": [ + { + "type": "null" + }, + { + "additionalProperties": false, + "properties": { + "total_count": { + "type": [ + "null", + "integer" + ] + }, + "workflows": { + "items": { + "additionalProperties": false, + "properties": { + "badge_url": { + "type": [ + "null", + "string" + ] + }, + "created_at": { + "format": "date-time", + "type": [ + "null", + "string" + ] + }, + "html_url": { + "type": [ + "null", + "string" + ] + }, + "id": { + "type": [ + "null", + "integer" + ] + }, + "name": { + "type": [ + "null", + "string" + ] + }, + "node_id": { + "type": [ + "null", + "string" + ] + }, + "path": { + "type": [ + "null", + "string" + ] + }, + "state": { + "type": [ + "null", + "string" + ] + }, + "updated_at": { + "format": "date-time", + "type": [ + "null", + "string" + ] + }, + "url": { + "type": [ + "null", + "string" + ] + } + }, + "type": [ + "null", + "object" + ] + }, + "type": [ + "null", + "array" + ] + } + }, + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "total_count": { + "type": "integer" + }, + "workflow_runs": { + "items": { + "additionalProperties": false, + "properties": { + "actor": { + "additionalProperties": false, + "properties": { + "avatar_url": { + "type": "string" + }, + "details": { + "additionalProperties": false, + "properties": { + "bio": { + "type": "string" + }, + "blog": { + "type": "string" + }, + "company": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "email": { + "type": "string" + }, + "followers": { + "type": "integer" + }, + "following": { + "type": "integer" + }, + "hireable": { + "type": "boolean" + }, + "location": { + "type": "string" + }, + "name": { + "type": "string" + }, + "owned_private_repos": { + "type": "integer" + }, + "private_gists": { + "type": "integer" + }, + "public_gists": { + "type": "integer" + }, + "public_repos": { + "type": "integer" + }, + "total_private_repos": { + "type": "integer" + }, + "twitter_username": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + }, + "required": [ + "public_repos", + "public_gists", + "followers", + "following", + "created_at", + "updated_at" + ], + "type": [ + "null", + "object" + ] + }, + "id": { + "type": "integer" + }, + "login": { + "type": "string" + }, + "profile_url": { + "type": "string" + } + }, + "required": [ + "login" + ], + "type": [ + "null", + "object" + ] + }, + "conclusion": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "display_title": { + "type": "string" + }, + "event": { + "type": "string" + }, + "head_branch": { + "type": "string" + }, + "head_commit": { + "additionalProperties": false, + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": [ + "null", + "object" + ] + }, + "head_sha": { + "type": "string" + }, + "html_url": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "pull_requests": { + "items": { + "type": "integer" + }, + "type": [ + "null", + "array" + ] + }, + "referenced_workflows": { + "items": { + "additionalProperties": false, + "properties": { + "path": { + "type": "string" + }, + "ref": { + "type": "string" + }, + "sha": { + "type": "string" + } + }, + "type": "object" + }, + "type": [ + "null", + "array" + ] + }, + "run_attempt": { + "type": "integer" + }, + "run_number": { + "type": "integer" + }, + "run_started_at": { + "type": "string" + }, + "status": { + "type": "string" + }, + "triggering_actor": { + "additionalProperties": false, + "properties": { + "avatar_url": { + "type": "string" + }, + "details": { + "additionalProperties": false, + "properties": { + "bio": { + "type": "string" + }, + "blog": { + "type": "string" + }, + "company": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "email": { + "type": "string" + }, + "followers": { + "type": "integer" + }, + "following": { + "type": "integer" + }, + "hireable": { + "type": "boolean" + }, + "location": { + "type": "string" + }, + "name": { + "type": "string" + }, + "owned_private_repos": { + "type": "integer" + }, + "private_gists": { + "type": "integer" + }, + "public_gists": { + "type": "integer" + }, + "public_repos": { + "type": "integer" + }, + "total_private_repos": { + "type": "integer" + }, + "twitter_username": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + }, + "required": [ + "public_repos", + "public_gists", + "followers", + "following", + "created_at", + "updated_at" + ], + "type": [ + "null", + "object" + ] + }, + "id": { + "type": "integer" + }, + "login": { + "type": "string" + }, + "profile_url": { + "type": "string" + } + }, + "required": [ + "login" + ], + "type": [ + "null", + "object" + ] + }, + "updated_at": { + "type": "string" + }, + "workflow_id": { + "type": "integer" + } + }, + "required": [ + "id", + "name", + "workflow_id", + "run_number", + "run_attempt", + "status" + ], + "type": "object" + }, + "type": [ + "null", + "array" + ] + } + }, + "required": [ + "total_count", + "workflow_runs" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "jobs": { + "additionalProperties": false, + "properties": { + "jobs": { + "items": { + "additionalProperties": false, + "properties": { + "completed_at": { + "type": "string" + }, + "conclusion": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "head_branch": { + "type": "string" + }, + "head_sha": { + "type": "string" + }, + "html_url": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "labels": { + "items": { + "type": "string" + }, + "type": [ + "null", + "array" + ] + }, + "name": { + "type": "string" + }, + "run_attempt": { + "type": "integer" + }, + "run_id": { + "type": "integer" + }, + "runner_group_id": { + "type": "integer" + }, + "runner_group_name": { + "type": "string" + }, + "runner_id": { + "type": "integer" + }, + "runner_name": { + "type": "string" + }, + "started_at": { + "type": "string" + }, + "status": { + "type": "string" + }, + "steps": { + "items": { + "additionalProperties": false, + "properties": { + "completed_at": { + "type": "string" + }, + "conclusion": { + "type": "string" + }, + "name": { + "type": "string" + }, + "number": { + "type": "integer" + }, + "started_at": { + "type": "string" + }, + "status": { + "type": "string" + } + }, + "required": [ + "name", + "status", + "number" + ], + "type": "object" + }, + "type": [ + "null", + "array" + ] + }, + "workflow_name": { + "type": "string" + } + }, + "required": [ + "id", + "run_id", + "name", + "status" + ], + "type": "object" + }, + "type": [ + "null", + "array" + ] + }, + "total_count": { + "type": "integer" + } + }, + "required": [ + "total_count", + "jobs" + ], + "type": "object" + } + }, + "required": [ + "jobs" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "artifacts": { + "items": { + "additionalProperties": false, + "properties": { + "archive_download_url": { + "type": [ + "null", + "string" + ] + }, + "created_at": { + "format": "date-time", + "type": [ + "null", + "string" + ] + }, + "digest": { + "type": [ + "null", + "string" + ] + }, + "expired": { + "type": [ + "null", + "boolean" + ] + }, + "expires_at": { + "format": "date-time", + "type": [ + "null", + "string" + ] + }, + "id": { + "type": [ + "null", + "integer" + ] + }, + "name": { + "type": [ + "null", + "string" + ] + }, + "node_id": { + "type": [ + "null", + "string" + ] + }, + "size_in_bytes": { + "type": [ + "null", + "integer" + ] + }, + "updated_at": { + "format": "date-time", + "type": [ + "null", + "string" + ] + }, + "url": { + "type": [ + "null", + "string" + ] + }, + "workflow_run": { + "additionalProperties": false, + "properties": { + "head_branch": { + "type": [ + "null", + "string" + ] + }, + "head_repository_id": { + "type": [ + "null", + "integer" + ] + }, + "head_sha": { + "type": [ + "null", + "string" + ] + }, + "id": { + "type": [ + "null", + "integer" + ] + }, + "repository_id": { + "type": [ + "null", + "integer" + ] + } + }, + "type": [ + "null", + "object" + ] + } + }, + "type": [ + "null", + "object" + ] + }, + "type": [ + "null", + "array" + ] + }, + "total_count": { + "type": [ + "null", + "integer" + ] + } + }, + "type": "object" + } + ] + } +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/actions_run_trigger.snap b/pkg/github/__toolsnaps__/actions_run_trigger.snap index 0c859b86ec..7871250898 100644 --- a/pkg/github/__toolsnaps__/actions_run_trigger.snap +++ b/pkg/github/__toolsnaps__/actions_run_trigger.snap @@ -14,14 +14,7 @@ "type": "object" }, "method": { - "description": "The method to execute", - "enum": [ - "run_workflow", - "rerun_workflow_run", - "rerun_failed_jobs", - "cancel_workflow_run", - "delete_workflow_run_logs" - ], + "description": "The method to execute Supported values: run_workflow, rerun_workflow_run, rerun_failed_jobs, cancel_workflow_run, delete_workflow_run_logs.", "type": "string" }, "owner": { @@ -52,5 +45,76 @@ ], "type": "object" }, - "name": "actions_run_trigger" + "name": "actions_run_trigger", + "outputSchema": { + "oneOf": [ + { + "type": "null" + }, + { + "additionalProperties": false, + "properties": { + "inputs": { + "additionalProperties": true, + "type": [ + "object", + "null" + ] + }, + "message": { + "type": "string" + }, + "ref": { + "type": "string" + }, + "status": { + "type": "string" + }, + "status_code": { + "type": "integer" + }, + "workflow_id": { + "type": "string" + }, + "workflow_type": { + "type": "string" + } + }, + "required": [ + "inputs", + "message", + "ref", + "status", + "status_code", + "workflow_id", + "workflow_type" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "message": { + "type": "string" + }, + "run_id": { + "type": "integer" + }, + "status": { + "type": "string" + }, + "status_code": { + "type": "integer" + } + }, + "required": [ + "message", + "run_id", + "status", + "status_code" + ], + "type": "object" + } + ] + } } \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/actions_run_trigger_typed.snap b/pkg/github/__toolsnaps__/actions_run_trigger_typed.snap new file mode 100644 index 0000000000..ccf4538201 --- /dev/null +++ b/pkg/github/__toolsnaps__/actions_run_trigger_typed.snap @@ -0,0 +1,134 @@ +{ + "annotations": { + "destructiveHint": true, + "idempotentHint": false, + "readOnlyHint": false, + "title": "Trigger GitHub Actions workflow actions" + }, + "description": "Trigger GitHub Actions workflow operations, including running, re-running, cancelling workflow runs, and deleting workflow run logs.", + "icons": [ + { + "mimeType": "image/png", + "src": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAABqUlEQVRIie2UMUtbURTHf+fmqdQKBR21k8+SF9AXDHTRyaWBbqWddLZCyWcwawc7ONUhW2YHBeNX6BSDxEB57ZJJkICgAZPHOx0iVfC+0hvJIHiWyz3nf87vcvnfCyMOAVgI8u8V3QNmU3Q9VA6IrzaiKLpxAXgAqvodoQNUbSKFVyK6xdjkMVBxBiDMIVKJzk7KaUI/CLcQ89plOIBxbXgGPF1ATxKdThNls9kZAJLE6Q3AnU0PFUp+EJZsoniw9E0mczQcoHe9zvjLI5Q3t8BVlBXg62DLlRhz+LNZb7gCxJb0c/kyqttRq2GvB0vLKqyJSsY+Vi/od6tRFN14rifyg6VlkB+i/KNXwJsqAp+cASjvELwJE880m82OTbKQDXdV9DOk2FSVPkChUBh7UDRmAiBtOIAa6QDj6QCoA1xexyUrxCGsV/S7dVKbD8J9EXYuu/GOH4T36Pp4AKC/Wo2P87nFoiTyFpEXfyt3Fn4UYAA5O60BtftJP5cvg/43YOR/0RA2Tdog+EH4DTi3a/QD0B4O0O9W8aaKiH7h1oqWaKvRTefZw8QfOA2GtnSwHcIAAAAASUVORK5CYII=", + "theme": "light" + }, + { + "mimeType": "image/png", + "src": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAABLElEQVRIie2Wr04DQRjEf8O/C4qEvgOWBIEpCkMTEgQBhUVgeAbOYjDoOhygSHgHFOEVKIaEFIO5IgbBXlJxu2WvKQkJY+7PNzszm/2+y8FvwPau7RfHUdm+tl3kaisYDIB34D7CWwFOgGNJ/TY7sO1yWk4T5rLT/Bv8WYMRsBoj2e6E26qVi+2bxJDVGNlez9WuB60AjoC18H4L6ALn4fkDuJP01MqgYUclcCYpVt8AtoH5iO4bcCWpWshNFMQfgElre8Bhmy7aCeIdRQBcAnsQb9PPkHaxoVYASBomQgyBpZTBY7ieRkymg23Zvk317IT1Zc1pPChJtn3A90FtAstj5bqFZ4PxdD/hzPxblD0HwADA9gXwGuHs17xs2C7CD0CV6IFn271WBrn4Aj4U/yN7l7QYAAAAAElFTkSuQmCC", + "theme": "dark" + } + ], + "inputSchema": { + "properties": { + "inputs": { + "description": "Inputs the workflow accepts. Only used for 'run_workflow' method.", + "properties": {}, + "type": "object" + }, + "method": { + "description": "The method to execute Supported values: run_workflow, rerun_workflow_run, rerun_failed_jobs, cancel_workflow_run, delete_workflow_run_logs.", + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string", + "x-mcp-header": "owner" + }, + "ref": { + "description": "The git reference for the workflow. The reference can be a branch or tag name. Required for 'run_workflow' method.", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string", + "x-mcp-header": "repo" + }, + "run_id": { + "description": "The ID of the workflow run. Required for all methods except 'run_workflow'.", + "type": "number" + }, + "workflow_id": { + "description": "The workflow ID (numeric) or workflow file name (e.g., main.yml, ci.yaml). Required for 'run_workflow' method.", + "type": "string" + } + }, + "required": [ + "method", + "owner", + "repo" + ], + "type": "object" + }, + "name": "actions_run_trigger", + "outputSchema": { + "oneOf": [ + { + "type": "null" + }, + { + "additionalProperties": false, + "properties": { + "inputs": { + "additionalProperties": true, + "type": [ + "object", + "null" + ] + }, + "message": { + "type": "string" + }, + "ref": { + "type": "string" + }, + "status": { + "type": "string" + }, + "status_code": { + "type": "integer" + }, + "workflow_id": { + "type": "string" + }, + "workflow_type": { + "type": "string" + } + }, + "required": [ + "inputs", + "message", + "ref", + "status", + "status_code", + "workflow_id", + "workflow_type" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "message": { + "type": "string" + }, + "run_id": { + "type": "integer" + }, + "status": { + "type": "string" + }, + "status_code": { + "type": "integer" + } + }, + "required": [ + "message", + "run_id", + "status", + "status_code" + ], + "type": "object" + } + ] + } +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/get_job_logs_typed.snap b/pkg/github/__toolsnaps__/get_job_logs_typed.snap new file mode 100644 index 0000000000..cc0c8a3426 --- /dev/null +++ b/pkg/github/__toolsnaps__/get_job_logs_typed.snap @@ -0,0 +1,249 @@ +{ + "annotations": { + "idempotentHint": false, + "readOnlyHint": true, + "title": "Get GitHub Actions workflow job logs" + }, + "description": "Get logs for GitHub Actions workflow jobs.\nUse this tool to retrieve logs for a specific job or all failed jobs in a workflow run.\nFor single job logs, provide job_id. For all failed jobs in a run, provide run_id with failed_only=true.\n", + "icons": [ + { + "mimeType": "image/png", + "src": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAABqUlEQVRIie2UMUtbURTHf+fmqdQKBR21k8+SF9AXDHTRyaWBbqWddLZCyWcwawc7ONUhW2YHBeNX6BSDxEB57ZJJkICgAZPHOx0iVfC+0hvJIHiWyz3nf87vcvnfCyMOAVgI8u8V3QNmU3Q9VA6IrzaiKLpxAXgAqvodoQNUbSKFVyK6xdjkMVBxBiDMIVKJzk7KaUI/CLcQ89plOIBxbXgGPF1ATxKdThNls9kZAJLE6Q3AnU0PFUp+EJZsoniw9E0mczQcoHe9zvjLI5Q3t8BVlBXg62DLlRhz+LNZb7gCxJb0c/kyqttRq2GvB0vLKqyJSsY+Vi/od6tRFN14rifyg6VlkB+i/KNXwJsqAp+cASjvELwJE880m82OTbKQDXdV9DOk2FSVPkChUBh7UDRmAiBtOIAa6QDj6QCoA1xexyUrxCGsV/S7dVKbD8J9EXYuu/GOH4T36Pp4AKC/Wo2P87nFoiTyFpEXfyt3Fn4UYAA5O60BtftJP5cvg/43YOR/0RA2Tdog+EH4DTi3a/QD0B4O0O9W8aaKiH7h1oqWaKvRTefZw8QfOA2GtnSwHcIAAAAASUVORK5CYII=", + "theme": "light" + }, + { + "mimeType": "image/png", + "src": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAABLElEQVRIie2Wr04DQRjEf8O/C4qEvgOWBIEpCkMTEgQBhUVgeAbOYjDoOhygSHgHFOEVKIaEFIO5IgbBXlJxu2WvKQkJY+7PNzszm/2+y8FvwPau7RfHUdm+tl3kaisYDIB34D7CWwFOgGNJ/TY7sO1yWk4T5rLT/Bv8WYMRsBoj2e6E26qVi+2bxJDVGNlez9WuB60AjoC18H4L6ALn4fkDuJP01MqgYUclcCYpVt8AtoH5iO4bcCWpWshNFMQfgElre8Bhmy7aCeIdRQBcAnsQb9PPkHaxoVYASBomQgyBpZTBY7ieRkymg23Zvk317IT1Zc1pPChJtn3A90FtAstj5bqFZ4PxdD/hzPxblD0HwADA9gXwGuHs17xs2C7CD0CV6IFn271WBrn4Aj4U/yN7l7QYAAAAAElFTkSuQmCC", + "theme": "dark" + } + ], + "inputSchema": { + "properties": { + "failed_only": { + "description": "When true, gets logs for all failed jobs in the workflow run specified by run_id. Requires run_id to be provided.", + "type": "boolean" + }, + "job_id": { + "description": "The unique identifier of the workflow job. Required when getting logs for a single job.", + "type": "number" + }, + "owner": { + "description": "Repository owner", + "type": "string", + "x-mcp-header": "owner" + }, + "repo": { + "description": "Repository name", + "type": "string", + "x-mcp-header": "repo" + }, + "return_content": { + "description": "Returns actual log content instead of URLs", + "type": "boolean" + }, + "run_id": { + "description": "The unique identifier of the workflow run. Required when failed_only is true to get logs for all failed jobs in the run.", + "type": "number" + }, + "tail_lines": { + "default": 500, + "description": "Number of lines to return from the end of the log", + "type": "number" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "name": "get_job_logs", + "outputSchema": { + "oneOf": [ + { + "type": "null" + }, + { + "additionalProperties": false, + "properties": { + "job_id": { + "type": "integer" + }, + "job_name": { + "type": "string" + }, + "logs_content": { + "type": "string" + }, + "message": { + "type": "string" + }, + "original_length": { + "type": "integer" + } + }, + "required": [ + "job_id", + "logs_content", + "message", + "original_length" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "job_id": { + "type": "integer" + }, + "job_name": { + "type": "string" + }, + "logs_url": { + "type": "string" + }, + "message": { + "type": "string" + }, + "note": { + "type": "string" + } + }, + "required": [ + "job_id", + "logs_url", + "message", + "note" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "failed_jobs": { + "type": "integer" + }, + "logs": { + "items": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "job_id": { + "type": "integer" + }, + "job_name": { + "type": "string" + }, + "logs_content": { + "type": "string" + }, + "message": { + "type": "string" + }, + "original_length": { + "type": "integer" + } + }, + "required": [ + "job_id", + "logs_content", + "message", + "original_length" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "job_id": { + "type": "integer" + }, + "job_name": { + "type": "string" + }, + "logs_url": { + "type": "string" + }, + "message": { + "type": "string" + }, + "note": { + "type": "string" + } + }, + "required": [ + "job_id", + "logs_url", + "message", + "note" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "error": { + "type": "string" + }, + "job_id": { + "type": "integer" + }, + "job_name": { + "type": "string" + } + }, + "required": [ + "error", + "job_id", + "job_name" + ], + "type": "object" + } + ] + }, + "type": [ + "null", + "array" + ] + }, + "message": { + "type": "string" + }, + "return_format": { + "additionalProperties": false, + "properties": { + "content": { + "type": "boolean" + }, + "urls": { + "type": "boolean" + } + }, + "required": [ + "content", + "urls" + ], + "type": [ + "null", + "object" + ] + }, + "run_id": { + "type": "integer" + }, + "total_jobs": { + "type": "integer" + } + }, + "required": [ + "failed_jobs", + "message", + "run_id", + "total_jobs" + ], + "type": "object" + } + ] + } +} \ No newline at end of file diff --git a/pkg/github/actions.go b/pkg/github/actions.go index 970e9fa31a..a8d3e2df10 100644 --- a/pkg/github/actions.go +++ b/pkg/github/actions.go @@ -47,7 +47,7 @@ const ( ) // handleFailedJobLogs gets logs for all failed jobs in a workflow run -func handleFailedJobLogs(ctx context.Context, client *github.Client, owner, repo string, runID int64, returnContent bool, tailLines int, contentWindowSize int) (*mcp.CallToolResult, any, error) { +func handleFailedJobLogs(ctx context.Context, client *github.Client, owner, repo string, runID int64, returnContent bool, tailLines int, contentWindowSize int) (*mcp.CallToolResult, *ActionsJobLogsOutput, error) { // First, get all jobs for the workflow run jobs, resp, err := client.Actions.ListWorkflowJobs(ctx, owner, repo, runID, &github.ListWorkflowJobsOptions{ Filter: "latest", @@ -66,41 +66,38 @@ func handleFailedJobLogs(ctx context.Context, client *github.Client, owner, repo } if len(failedJobs) == 0 { - result := map[string]any{ - "message": "No failed jobs found in this workflow run", - "run_id": runID, - "total_jobs": len(jobs.Jobs), - "failed_jobs": 0, + result := &ActionsFailedJobLogsOutput{ + Message: "No failed jobs found in this workflow run", + RunID: runID, + TotalJobs: len(jobs.Jobs), } r, _ := json.Marshal(result) - return utils.NewToolResultText(string(r)), nil, nil + return utils.NewToolResultText(string(r)), &ActionsJobLogsOutput{Failed: result}, nil } // Collect logs for all failed jobs - var logResults []map[string]any + var logResults []ActionsJobLog for _, job := range failedJobs { jobResult, resp, err := getJobLogData(ctx, client, owner, repo, job.GetID(), job.GetName(), returnContent, tailLines, contentWindowSize) if err != nil { // Continue with other jobs even if one fails - jobResult = map[string]any{ - "job_id": job.GetID(), - "job_name": job.GetName(), - "error": err.Error(), - } + jobResult = &ActionsJobLog{Error: &ActionsJobLogError{ + JobID: job.GetID(), JobName: job.GetName(), Error: err.Error(), + }} // Enable reporting of status codes and error causes _, _ = ghErrors.NewGitHubAPIErrorToCtx(ctx, "failed to get job logs", resp, err) // Explicitly ignore error for graceful handling } - logResults = append(logResults, jobResult) + logResults = append(logResults, *jobResult) } - result := map[string]any{ - "message": fmt.Sprintf("Retrieved logs for %d failed jobs", len(failedJobs)), - "run_id": runID, - "total_jobs": len(jobs.Jobs), - "failed_jobs": len(failedJobs), - "logs": logResults, - "return_format": map[string]bool{"content": returnContent, "urls": !returnContent}, + result := &ActionsFailedJobLogsOutput{ + Message: fmt.Sprintf("Retrieved logs for %d failed jobs", len(failedJobs)), + RunID: runID, + TotalJobs: len(jobs.Jobs), + FailedJobs: len(failedJobs), + Logs: &logResults, + ReturnFormat: &ActionsLogsReturnFormat{Content: returnContent, URLs: !returnContent}, } r, err := json.Marshal(result) @@ -108,11 +105,11 @@ func handleFailedJobLogs(ctx context.Context, client *github.Client, owner, repo return nil, nil, fmt.Errorf("failed to marshal response: %w", err) } - return utils.NewToolResultText(string(r)), nil, nil + return utils.NewToolResultText(string(r)), &ActionsJobLogsOutput{Failed: result}, nil } // handleSingleJobLogs gets logs for a single job -func handleSingleJobLogs(ctx context.Context, client *github.Client, owner, repo string, jobID int64, returnContent bool, tailLines int, contentWindowSize int) (*mcp.CallToolResult, any, error) { +func handleSingleJobLogs(ctx context.Context, client *github.Client, owner, repo string, jobID int64, returnContent bool, tailLines int, contentWindowSize int) (*mcp.CallToolResult, *ActionsJobLogsOutput, error) { jobResult, resp, err := getJobLogData(ctx, client, owner, repo, jobID, "", returnContent, tailLines, contentWindowSize) if err != nil { return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to get job logs", resp, err), nil, nil @@ -123,11 +120,11 @@ func handleSingleJobLogs(ctx context.Context, client *github.Client, owner, repo return nil, nil, fmt.Errorf("failed to marshal response: %w", err) } - return utils.NewToolResultText(string(r)), nil, nil + return utils.NewToolResultText(string(r)), &ActionsJobLogsOutput{Single: jobResult}, nil } // getJobLogData retrieves log data for a single job, either as URL or content -func getJobLogData(ctx context.Context, client *github.Client, owner, repo string, jobID int64, jobName string, returnContent bool, tailLines int, contentWindowSize int) (map[string]any, *github.Response, error) { +func getJobLogData(ctx context.Context, client *github.Client, owner, repo string, jobID int64, jobName string, returnContent bool, tailLines int, contentWindowSize int) (*ActionsJobLog, *github.Response, error) { // Get the download URL for the job logs url, resp, err := client.Actions.GetWorkflowJobLogs(ctx, owner, repo, jobID, 1) if err != nil { @@ -135,13 +132,7 @@ func getJobLogData(ctx context.Context, client *github.Client, owner, repo strin } defer func() { _ = resp.Body.Close() }() - result := map[string]any{ - "job_id": jobID, - } - if jobName != "" { - result["job_name"] = jobName - } - + result := &ActionsJobLog{} if returnContent { // Download and return the actual log content content, originalLength, httpResp, err := downloadLogContent(ctx, url.String(), tailLines, contentWindowSize) //nolint:bodyclose // Response body is closed in downloadLogContent, but we need to return httpResp @@ -152,14 +143,17 @@ func getJobLogData(ctx context.Context, client *github.Client, owner, repo strin } return nil, ghResp, fmt.Errorf("failed to download log content for job %d: %w", jobID, err) } - result["logs_content"] = content - result["message"] = "Job logs content retrieved successfully" - result["original_length"] = originalLength + result.Content = &ActionsJobLogContent{ + JobID: jobID, JobName: jobName, LogsContent: content, + Message: "Job logs content retrieved successfully", OriginalLength: originalLength, + } } else { // Return just the URL - result["logs_url"] = url.String() - result["message"] = "Job logs are available for download" - result["note"] = "The logs_url provides a download link for the individual job logs in plain text format. Use return_content=true to get the actual log content." + result.URL = &ActionsJobLogURL{ + JobID: jobID, JobName: jobName, LogsURL: url.String(), + Message: "Job logs are available for download", + Note: "The logs_url provides a download link for the individual job logs in plain text format. Use return_content=true to get the actual log content.", + } } return result, resp, nil @@ -202,7 +196,8 @@ func ActionsList(t translations.TranslationHelperFunc) inventory.ServerTool { tool := NewTool( ToolsetMetadataActions, mcp.Tool{ - Name: "actions_list", + Name: "actions_list", + OutputSchema: actionsListOutputSchema(), Description: t("TOOL_ACTIONS_LIST_DESCRIPTION", `Tools for listing GitHub Actions resources. Use this tool to list workflows in a repository, or list workflow runs, jobs, and artifacts for a specific workflow or workflow run. @@ -324,31 +319,9 @@ Use this tool to list workflows in a repository, or list workflow runs, jobs, an }, }, scopes.PublicRead(scopes.Repo), - func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - repo, err := RequiredParam[string](args, "repo") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - method, err := RequiredParam[string](args, "method") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - resourceID, err := OptionalParam[string](args, "resource_id") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - pagination, err := OptionalPaginationParams(args) - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args ActionsListInput) (*mcp.CallToolResult, *ActionsListOutput, error) { + owner, repo, method, resourceID := args.Owner, args.Repo, args.Method, args.ResourceID + pagination := PaginationParams{Page: args.Page, PerPage: args.PerPage} client, err := deps.GetClient(ctx) if err != nil { @@ -388,10 +361,10 @@ Use this tool to list workflows in a repository, or list workflow runs, jobs, an result, payload, err := listWorkflows(ctx, client, owner, repo, pagination) return attachIFC(result), payload, err case actionsMethodListWorkflowRuns: - result, payload, err := listWorkflowRuns(ctx, client, args, owner, repo, resourceID, pagination) + result, payload, err := listWorkflowRuns(ctx, client, args.WorkflowRunsFilter, owner, repo, resourceID, pagination) return attachIFC(result), payload, err case actionsMethodListWorkflowJobs: - result, payload, err := listWorkflowJobs(ctx, client, args, owner, repo, resourceIDInt, pagination) + result, payload, err := listWorkflowJobs(ctx, client, args.WorkflowJobsFilter, owner, repo, resourceIDInt, pagination) return attachIFC(result), payload, err case actionsMethodListWorkflowArtifacts: result, payload, err := listWorkflowArtifacts(ctx, client, owner, repo, resourceIDInt, pagination) @@ -400,8 +373,9 @@ Use this tool to list workflows in a repository, or list workflow runs, jobs, an return utils.NewToolResultError(fmt.Sprintf("unknown method: %s", method)), nil, nil } }, + normalizeActionsArguments("list"), ) - return tool + return actionsInputSchema(tool) } // ActionsGet returns the tool and handler for getting GitHub Actions resources. @@ -409,7 +383,8 @@ func ActionsGet(t translations.TranslationHelperFunc) inventory.ServerTool { tool := NewTool( ToolsetMetadataActions, mcp.Tool{ - Name: "actions_get", + Name: "actions_get", + OutputSchema: actionsGetOutputSchema(), Description: t("TOOL_ACTIONS_GET_DESCRIPTION", `Get details about specific GitHub Actions resources. Use this tool to get details about individual workflows, workflow runs, jobs, and artifacts by their unique IDs. `), @@ -454,24 +429,8 @@ Use this tool to get details about individual workflows, workflow runs, jobs, an }, }, scopes.PublicRead(scopes.Repo), - func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - repo, err := RequiredParam[string](args, "repo") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - method, err := RequiredParam[string](args, "method") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - resourceID, err := RequiredParam[string](args, "resource_id") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args ActionsGetInput) (*mcp.CallToolResult, *ActionsGetOutput, error) { + owner, repo, method, resourceID := args.Owner, args.Repo, args.Method, args.ResourceID client, err := deps.GetClient(ctx) if err != nil { @@ -522,8 +481,9 @@ Use this tool to get details about individual workflows, workflow runs, jobs, an return utils.NewToolResultError(fmt.Sprintf("unknown method: %s", method)), nil, nil } }, + normalizeActionsArguments("get"), ) - return tool + return actionsInputSchema(tool) } // ActionsRunTrigger returns the tool and handler for triggering GitHub Actions workflows. @@ -531,8 +491,9 @@ func ActionsRunTrigger(t translations.TranslationHelperFunc) inventory.ServerToo tool := NewTool( ToolsetMetadataActions, mcp.Tool{ - Name: "actions_run_trigger", - Description: t("TOOL_ACTIONS_RUN_TRIGGER_DESCRIPTION", "Trigger GitHub Actions workflow operations, including running, re-running, cancelling workflow runs, and deleting workflow run logs."), + Name: "actions_run_trigger", + OutputSchema: actionsRunTriggerOutputSchema(), + Description: t("TOOL_ACTIONS_RUN_TRIGGER_DESCRIPTION", "Trigger GitHub Actions workflow operations, including running, re-running, cancelling workflow runs, and deleting workflow run logs."), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_ACTIONS_RUN_TRIGGER_USER_TITLE", "Trigger GitHub Actions workflow actions"), ReadOnlyHint: false, @@ -582,30 +543,9 @@ func ActionsRunTrigger(t translations.TranslationHelperFunc) inventory.ServerToo }, }, scopes.RequireAll(scopes.Repo), - func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - repo, err := RequiredParam[string](args, "repo") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - method, err := RequiredParam[string](args, "method") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - // Get optional parameters - workflowID, _ := OptionalParam[string](args, "workflow_id") - ref, _ := OptionalParam[string](args, "ref") - runID, _ := OptionalIntParam(args, "run_id") - - // Get optional inputs parameter - inputs, err := OptionalParam[map[string]any](args, "inputs") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } + func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args ActionsRunTriggerInput) (*mcp.CallToolResult, *ActionsRunTriggerOutput, error) { + owner, repo, method := args.Owner, args.Repo, args.Method + workflowID, ref, runID, inputs := args.WorkflowID, args.Ref, args.RunID, args.Inputs // Validate required parameters based on action type if method == actionsMethodRunWorkflow { @@ -639,8 +579,9 @@ func ActionsRunTrigger(t translations.TranslationHelperFunc) inventory.ServerToo return utils.NewToolResultError(fmt.Sprintf("unknown method: %s", method)), nil, nil } }, + normalizeActionsArguments("trigger"), ) - return tool + return actionsInputSchema(tool) } // ActionsGetJobLogs returns the tool and handler for getting workflow job logs. @@ -648,7 +589,8 @@ func ActionsGetJobLogs(t translations.TranslationHelperFunc) inventory.ServerToo tool := NewTool( ToolsetMetadataActions, mcp.Tool{ - Name: "get_job_logs", + Name: "get_job_logs", + OutputSchema: actionsJobLogsOutputSchema(), Description: t("TOOL_GET_JOB_LOGS_CONSOLIDATED_DESCRIPTION", `Get logs for GitHub Actions workflow jobs. Use this tool to retrieve logs for a specific job or all failed jobs in a workflow run. For single job logs, provide job_id. For all failed jobs in a run, provide run_id with failed_only=true. @@ -694,44 +636,9 @@ For single job logs, provide job_id. For all failed jobs in a run, provide run_i }, }, scopes.PublicRead(scopes.Repo), - func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - owner, err := RequiredParam[string](args, "owner") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - repo, err := RequiredParam[string](args, "repo") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - jobID, err := OptionalIntParam(args, "job_id") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - runID, err := OptionalIntParam(args, "run_id") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - failedOnly, err := OptionalParam[bool](args, "failed_only") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - returnContent, err := OptionalParam[bool](args, "return_content") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - tailLines, err := OptionalIntParam(args, "tail_lines") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - // Default to 500 lines if not specified or invalid - if tailLines <= 0 { - tailLines = 500 - } + func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args ActionsGetJobLogsInput) (*mcp.CallToolResult, *ActionsJobLogsOutput, error) { + owner, repo, jobID, runID := args.Owner, args.Repo, args.JobID, args.RunID + failedOnly, returnContent, tailLines := args.FailedOnly, args.ReturnContent, args.TailLines client, err := deps.GetClient(ctx) if err != nil { @@ -766,13 +673,14 @@ For single job logs, provide job_id. For all failed jobs in a run, provide run_i return utils.NewToolResultError("Either job_id must be provided for single job logs, or run_id with failed_only=true for failed job logs"), nil, nil }, + normalizeActionsArguments("logs"), ) - return tool + return actionsInputSchema(tool) } // Helper functions for consolidated actions tools -func getWorkflow(ctx context.Context, client *github.Client, owner, repo, resourceID string) (*mcp.CallToolResult, any, error) { +func getWorkflow(ctx context.Context, client *github.Client, owner, repo, resourceID string) (*mcp.CallToolResult, *ActionsGetOutput, error) { var workflow *github.Workflow var resp *github.Response var err error @@ -793,23 +701,24 @@ func getWorkflow(ctx context.Context, client *github.Client, owner, repo, resour return nil, nil, fmt.Errorf("failed to marshal workflow: %w", err) } - return utils.NewToolResultText(string(r)), nil, nil + return utils.NewToolResultText(string(r)), &ActionsGetOutput{Workflow: workflow}, nil } -func getWorkflowRun(ctx context.Context, client *github.Client, owner, repo string, resourceID int64) (*mcp.CallToolResult, any, error) { +func getWorkflowRun(ctx context.Context, client *github.Client, owner, repo string, resourceID int64) (*mcp.CallToolResult, *ActionsGetOutput, error) { workflowRun, resp, err := client.Actions.GetWorkflowRunByID(ctx, owner, repo, resourceID) if err != nil { return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to get workflow run", resp, err), nil, nil } defer func() { _ = resp.Body.Close() }() - r, err := json.Marshal(convertToMinimalWorkflowRun(workflowRun)) + output := convertToMinimalWorkflowRun(workflowRun) + r, err := json.Marshal(output) if err != nil { return nil, nil, fmt.Errorf("failed to marshal workflow run: %w", err) } - return utils.NewToolResultText(string(r)), nil, nil + return utils.NewToolResultText(string(r)), &ActionsGetOutput{Run: &output}, nil } -func getWorkflowJob(ctx context.Context, client *github.Client, owner, repo string, resourceID int64) (*mcp.CallToolResult, any, error) { +func getWorkflowJob(ctx context.Context, client *github.Client, owner, repo string, resourceID int64) (*mcp.CallToolResult, *ActionsGetOutput, error) { workflowJob, resp, err := client.Actions.GetWorkflowJobByID(ctx, owner, repo, resourceID) if err != nil { return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to get workflow job", resp, err), nil, nil @@ -819,10 +728,10 @@ func getWorkflowJob(ctx context.Context, client *github.Client, owner, repo stri if err != nil { return nil, nil, fmt.Errorf("failed to marshal workflow job: %w", err) } - return utils.NewToolResultText(string(r)), nil, nil + return utils.NewToolResultText(string(r)), &ActionsGetOutput{Job: workflowJob}, nil } -func listWorkflows(ctx context.Context, client *github.Client, owner, repo string, pagination PaginationParams) (*mcp.CallToolResult, any, error) { +func listWorkflows(ctx context.Context, client *github.Client, owner, repo string, pagination PaginationParams) (*mcp.CallToolResult, *ActionsListOutput, error) { opts := &github.ListOptions{ PerPage: pagination.PerPage, Page: pagination.Page, @@ -839,29 +748,19 @@ func listWorkflows(ctx context.Context, client *github.Client, owner, repo strin return nil, nil, fmt.Errorf("failed to marshal workflows: %w", err) } - return utils.NewToolResultText(string(r)), nil, nil + return utils.NewToolResultText(string(r)), &ActionsListOutput{Workflows: workflows}, nil } -func listWorkflowRuns(ctx context.Context, client *github.Client, args map[string]any, owner, repo, resourceID string, pagination PaginationParams) (*mcp.CallToolResult, any, error) { - filterArgs, err := OptionalParam[map[string]any](args, "workflow_runs_filter") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - filterArgsTyped := make(map[string]string) - for k, v := range filterArgs { - if strVal, ok := v.(string); ok { - filterArgsTyped[k] = strVal - } else { - filterArgsTyped[k] = "" - } +func listWorkflowRuns(ctx context.Context, client *github.Client, filter ActionsWorkflowRunsFilter, owner, repo, resourceID string, pagination PaginationParams) (*mcp.CallToolResult, *ActionsListOutput, error) { + if filter.validationError != "" { + return utils.NewToolResultError(filter.validationError), nil, nil } listWorkflowRunsOptions := &github.ListWorkflowRunsOptions{ - Actor: filterArgsTyped["actor"], - Branch: filterArgsTyped["branch"], - Event: filterArgsTyped["event"], - Status: filterArgsTyped["status"], + Actor: filter.Actor, + Branch: filter.Branch, + Event: filter.Event, + Status: filter.Status, ListOptions: github.ListOptions{ Page: pagination.Page, PerPage: pagination.PerPage, @@ -870,6 +769,7 @@ func listWorkflowRuns(ctx context.Context, client *github.Client, args map[strin var workflowRuns *github.WorkflowRuns var resp *github.Response + var err error if resourceID == "" { workflowRuns, resp, err = client.Actions.ListRepositoryWorkflowRuns(ctx, owner, repo, listWorkflowRunsOptions) @@ -884,31 +784,22 @@ func listWorkflowRuns(ctx context.Context, client *github.Client, args map[strin } defer func() { _ = resp.Body.Close() }() - r, err := json.Marshal(convertToMinimalWorkflowRuns(workflowRuns)) + output := convertToMinimalWorkflowRuns(workflowRuns) + r, err := json.Marshal(output) if err != nil { return nil, nil, fmt.Errorf("failed to marshal workflow runs: %w", err) } - return utils.NewToolResultText(string(r)), nil, nil + return utils.NewToolResultText(string(r)), &ActionsListOutput{Runs: &output}, nil } -func listWorkflowJobs(ctx context.Context, client *github.Client, args map[string]any, owner, repo string, resourceID int64, pagination PaginationParams) (*mcp.CallToolResult, any, error) { - filterArgs, err := OptionalParam[map[string]any](args, "workflow_jobs_filter") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - - filterArgsTyped := make(map[string]string) - for k, v := range filterArgs { - if strVal, ok := v.(string); ok { - filterArgsTyped[k] = strVal - } else { - filterArgsTyped[k] = "" - } +func listWorkflowJobs(ctx context.Context, client *github.Client, filter ActionsWorkflowJobsFilter, owner, repo string, resourceID int64, pagination PaginationParams) (*mcp.CallToolResult, *ActionsListOutput, error) { + if filter.validationError != "" { + return utils.NewToolResultError(filter.validationError), nil, nil } workflowJobs, resp, err := client.Actions.ListWorkflowJobs(ctx, owner, repo, resourceID, &github.ListWorkflowJobsOptions{ - Filter: filterArgsTyped["filter"], + Filter: filter.Filter, ListOptions: github.ListOptions{ Page: pagination.Page, PerPage: pagination.PerPage, @@ -918,8 +809,8 @@ func listWorkflowJobs(ctx context.Context, client *github.Client, args map[strin return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to list workflow jobs", resp, err), nil, nil } - response := map[string]any{ - "jobs": convertToMinimalWorkflowJobs(workflowJobs), + response := &ActionsJobsOutput{ + Jobs: convertToMinimalWorkflowJobs(workflowJobs), } defer func() { _ = resp.Body.Close() }() @@ -928,10 +819,10 @@ func listWorkflowJobs(ctx context.Context, client *github.Client, args map[strin return nil, nil, fmt.Errorf("failed to marshal workflow jobs: %w", err) } - return utils.NewToolResultText(string(r)), nil, nil + return utils.NewToolResultText(string(r)), &ActionsListOutput{Jobs: response}, nil } -func listWorkflowArtifacts(ctx context.Context, client *github.Client, owner, repo string, resourceID int64, pagination PaginationParams) (*mcp.CallToolResult, any, error) { +func listWorkflowArtifacts(ctx context.Context, client *github.Client, owner, repo string, resourceID int64, pagination PaginationParams) (*mcp.CallToolResult, *ActionsListOutput, error) { opts := &github.ListOptions{ PerPage: pagination.PerPage, Page: pagination.Page, @@ -948,10 +839,10 @@ func listWorkflowArtifacts(ctx context.Context, client *github.Client, owner, re return nil, nil, fmt.Errorf("failed to marshal response: %w", err) } - return utils.NewToolResultText(string(r)), nil, nil + return utils.NewToolResultText(string(r)), &ActionsListOutput{Artifacts: artifacts}, nil } -func downloadWorkflowArtifact(ctx context.Context, client *github.Client, owner, repo string, resourceID int64) (*mcp.CallToolResult, any, error) { +func downloadWorkflowArtifact(ctx context.Context, client *github.Client, owner, repo string, resourceID int64) (*mcp.CallToolResult, *ActionsGetOutput, error) { // Get the download URL for the artifact url, resp, err := client.Actions.DownloadArtifact(ctx, owner, repo, resourceID, 1) if err != nil { @@ -960,11 +851,11 @@ func downloadWorkflowArtifact(ctx context.Context, client *github.Client, owner, defer func() { _ = resp.Body.Close() }() // Create response with the download URL and information - result := map[string]any{ - "download_url": url.String(), - "message": "Artifact is available for download", - "note": "The download_url provides a download link for the artifact as a ZIP archive. The link is temporary and expires after a short time.", - "artifact_id": resourceID, + result := &ActionsArtifactDownloadOutput{ + DownloadURL: url.String(), + Message: "Artifact is available for download", + Note: "The download_url provides a download link for the artifact as a ZIP archive. The link is temporary and expires after a short time.", + ArtifactID: resourceID, } r, err := json.Marshal(result) @@ -972,10 +863,10 @@ func downloadWorkflowArtifact(ctx context.Context, client *github.Client, owner, return nil, nil, fmt.Errorf("failed to marshal response: %w", err) } - return utils.NewToolResultText(string(r)), nil, nil + return utils.NewToolResultText(string(r)), &ActionsGetOutput{Artifact: result}, nil } -func getWorkflowRunLogsURL(ctx context.Context, client *github.Client, owner, repo string, runID int64) (*mcp.CallToolResult, any, error) { +func getWorkflowRunLogsURL(ctx context.Context, client *github.Client, owner, repo string, runID int64) (*mcp.CallToolResult, *ActionsGetOutput, error) { // Get the download URL for the logs url, resp, err := client.Actions.GetWorkflowRunLogs(ctx, owner, repo, runID, 1) if err != nil { @@ -984,12 +875,12 @@ func getWorkflowRunLogsURL(ctx context.Context, client *github.Client, owner, re defer func() { _ = resp.Body.Close() }() // Create response with the logs URL and information - result := map[string]any{ - "logs_url": url.String(), - "message": "Workflow run logs are available for download", - "note": "The logs_url provides a download link for the complete workflow run logs as a ZIP archive. You can download this archive to extract and examine individual job logs.", - "warning": "This downloads ALL logs as a ZIP file which can be large and expensive. For debugging failed jobs, consider using get_job_logs with failed_only=true and run_id instead.", - "optimization_tip": "Use: get_job_logs with parameters {run_id: " + fmt.Sprintf("%d", runID) + ", failed_only: true} for more efficient failed job debugging", + result := &ActionsRunLogsOutput{ + LogsURL: url.String(), + Message: "Workflow run logs are available for download", + Note: "The logs_url provides a download link for the complete workflow run logs as a ZIP archive. You can download this archive to extract and examine individual job logs.", + Warning: "This downloads ALL logs as a ZIP file which can be large and expensive. For debugging failed jobs, consider using get_job_logs with failed_only=true and run_id instead.", + OptimizationTip: "Use: get_job_logs with parameters {run_id: " + fmt.Sprintf("%d", runID) + ", failed_only: true} for more efficient failed job debugging", } r, err := json.Marshal(result) @@ -997,10 +888,10 @@ func getWorkflowRunLogsURL(ctx context.Context, client *github.Client, owner, re return nil, nil, fmt.Errorf("failed to marshal response: %w", err) } - return utils.NewToolResultText(string(r)), nil, nil + return utils.NewToolResultText(string(r)), &ActionsGetOutput{Logs: result}, nil } -func getWorkflowRunUsage(ctx context.Context, client *github.Client, owner, repo string, resourceID int64) (*mcp.CallToolResult, any, error) { +func getWorkflowRunUsage(ctx context.Context, client *github.Client, owner, repo string, resourceID int64) (*mcp.CallToolResult, *ActionsGetOutput, error) { usage, resp, err := client.Actions.GetWorkflowRunUsageByID(ctx, owner, repo, resourceID) if err != nil { return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to get workflow run usage", resp, err), nil, nil @@ -1012,10 +903,10 @@ func getWorkflowRunUsage(ctx context.Context, client *github.Client, owner, repo return nil, nil, fmt.Errorf("failed to marshal response: %w", err) } - return utils.NewToolResultText(string(r)), nil, nil + return utils.NewToolResultText(string(r)), &ActionsGetOutput{Usage: usage}, nil } -func runWorkflow(ctx context.Context, client *github.Client, owner, repo, workflowID, ref string, inputs map[string]any) (*mcp.CallToolResult, any, error) { +func runWorkflow(ctx context.Context, client *github.Client, owner, repo, workflowID, ref string, inputs map[string]any) (*mcp.CallToolResult, *ActionsRunTriggerOutput, error) { event := github.CreateWorkflowDispatchEventRequest{ Ref: ref, Inputs: inputs, @@ -1038,14 +929,18 @@ func runWorkflow(ctx context.Context, client *github.Client, owner, repo, workfl } defer func() { _ = resp.Body.Close() }() - result := map[string]any{ - "message": "Workflow run has been queued", - "workflow_type": workflowType, - "workflow_id": workflowID, - "ref": ref, - "inputs": inputs, - "status": resp.Status, - "status_code": resp.StatusCode, + inputJSON, err := json.Marshal(inputs) + if err != nil { + return nil, nil, fmt.Errorf("failed to marshal response: %w", err) + } + result := &ActionsDispatchOutput{ + Message: "Workflow run has been queued", + WorkflowType: workflowType, + WorkflowID: workflowID, + Ref: ref, + Inputs: inputJSON, + Status: resp.Status, + StatusCode: resp.StatusCode, } r, err := json.Marshal(result) @@ -1053,21 +948,21 @@ func runWorkflow(ctx context.Context, client *github.Client, owner, repo, workfl return nil, nil, fmt.Errorf("failed to marshal response: %w", err) } - return utils.NewToolResultText(string(r)), nil, nil + return utils.NewToolResultText(string(r)), &ActionsRunTriggerOutput{Dispatch: result}, nil } -func rerunWorkflowRun(ctx context.Context, client *github.Client, owner, repo string, runID int64) (*mcp.CallToolResult, any, error) { +func rerunWorkflowRun(ctx context.Context, client *github.Client, owner, repo string, runID int64) (*mcp.CallToolResult, *ActionsRunTriggerOutput, error) { resp, err := client.Actions.RerunWorkflowByID(ctx, owner, repo, runID) if err != nil { return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to rerun workflow run", resp, err), nil, nil } defer func() { _ = resp.Body.Close() }() - result := map[string]any{ - "message": "Workflow run has been queued for re-run", - "run_id": runID, - "status": resp.Status, - "status_code": resp.StatusCode, + result := &ActionsRunOperationOutput{ + Message: "Workflow run has been queued for re-run", + RunID: runID, + Status: resp.Status, + StatusCode: resp.StatusCode, } r, err := json.Marshal(result) @@ -1075,21 +970,21 @@ func rerunWorkflowRun(ctx context.Context, client *github.Client, owner, repo st return nil, nil, fmt.Errorf("failed to marshal response: %w", err) } - return utils.NewToolResultText(string(r)), nil, nil + return utils.NewToolResultText(string(r)), &ActionsRunTriggerOutput{Run: result}, nil } -func rerunFailedJobs(ctx context.Context, client *github.Client, owner, repo string, runID int64) (*mcp.CallToolResult, any, error) { +func rerunFailedJobs(ctx context.Context, client *github.Client, owner, repo string, runID int64) (*mcp.CallToolResult, *ActionsRunTriggerOutput, error) { resp, err := client.Actions.RerunFailedJobsByID(ctx, owner, repo, runID) if err != nil { return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to rerun failed jobs", resp, err), nil, nil } defer func() { _ = resp.Body.Close() }() - result := map[string]any{ - "message": "Failed jobs have been queued for re-run", - "run_id": runID, - "status": resp.Status, - "status_code": resp.StatusCode, + result := &ActionsRunOperationOutput{ + Message: "Failed jobs have been queued for re-run", + RunID: runID, + Status: resp.Status, + StatusCode: resp.StatusCode, } r, err := json.Marshal(result) @@ -1097,10 +992,10 @@ func rerunFailedJobs(ctx context.Context, client *github.Client, owner, repo str return nil, nil, fmt.Errorf("failed to marshal response: %w", err) } - return utils.NewToolResultText(string(r)), nil, nil + return utils.NewToolResultText(string(r)), &ActionsRunTriggerOutput{Run: result}, nil } -func cancelWorkflowRun(ctx context.Context, client *github.Client, owner, repo string, runID int64) (*mcp.CallToolResult, any, error) { +func cancelWorkflowRun(ctx context.Context, client *github.Client, owner, repo string, runID int64) (*mcp.CallToolResult, *ActionsRunTriggerOutput, error) { resp, err := client.Actions.CancelWorkflowRunByID(ctx, owner, repo, runID) if err != nil { if _, ok := errors.AsType[*github.AcceptedError](err); !ok { @@ -1109,11 +1004,11 @@ func cancelWorkflowRun(ctx context.Context, client *github.Client, owner, repo s } defer func() { _ = resp.Body.Close() }() - result := map[string]any{ - "message": "Workflow run has been cancelled", - "run_id": runID, - "status": resp.Status, - "status_code": resp.StatusCode, + result := &ActionsRunOperationOutput{ + Message: "Workflow run has been cancelled", + RunID: runID, + Status: resp.Status, + StatusCode: resp.StatusCode, } r, err := json.Marshal(result) @@ -1121,21 +1016,21 @@ func cancelWorkflowRun(ctx context.Context, client *github.Client, owner, repo s return nil, nil, fmt.Errorf("failed to marshal response: %w", err) } - return utils.NewToolResultText(string(r)), nil, nil + return utils.NewToolResultText(string(r)), &ActionsRunTriggerOutput{Run: result}, nil } -func deleteWorkflowRunLogs(ctx context.Context, client *github.Client, owner, repo string, runID int64) (*mcp.CallToolResult, any, error) { +func deleteWorkflowRunLogs(ctx context.Context, client *github.Client, owner, repo string, runID int64) (*mcp.CallToolResult, *ActionsRunTriggerOutput, error) { resp, err := client.Actions.DeleteWorkflowRunLogs(ctx, owner, repo, runID) if err != nil { return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to delete workflow run logs", resp, err), nil, nil } defer func() { _ = resp.Body.Close() }() - result := map[string]any{ - "message": "Workflow run logs have been deleted", - "run_id": runID, - "status": resp.Status, - "status_code": resp.StatusCode, + result := &ActionsRunOperationOutput{ + Message: "Workflow run logs have been deleted", + RunID: runID, + Status: resp.Status, + StatusCode: resp.StatusCode, } r, err := json.Marshal(result) @@ -1143,5 +1038,5 @@ func deleteWorkflowRunLogs(ctx context.Context, client *github.Client, owner, re return nil, nil, fmt.Errorf("failed to marshal response: %w", err) } - return utils.NewToolResultText(string(r)), nil, nil + return utils.NewToolResultText(string(r)), &ActionsRunTriggerOutput{Run: result}, nil } diff --git a/pkg/github/actions_types.go b/pkg/github/actions_types.go new file mode 100644 index 0000000000..a4b746e449 --- /dev/null +++ b/pkg/github/actions_types.go @@ -0,0 +1,455 @@ +package github + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/github/github-mcp-server/pkg/inventory" + "github.com/google/go-github/v92/github" + "github.com/google/jsonschema-go/jsonschema" +) + +type ActionsListInput struct { + Method string `json:"method"` + Owner string `json:"owner"` + Repo string `json:"repo"` + ResourceID string `json:"resource_id,omitempty"` + Page int `json:"page,omitempty"` + PerPage int `json:"perPage,omitempty"` + WorkflowRunsFilter ActionsWorkflowRunsFilter `json:"workflow_runs_filter"` + WorkflowJobsFilter ActionsWorkflowJobsFilter `json:"workflow_jobs_filter"` +} + +type ActionsWorkflowRunsFilter struct { + Actor string `json:"actor,omitempty"` + Branch string `json:"branch,omitempty"` + Event string `json:"event,omitempty"` + Status string `json:"status,omitempty"` + validationError string +} + +type ActionsWorkflowJobsFilter struct { + Filter string `json:"filter,omitempty"` + validationError string +} + +// The legacy handler validates filters only after client acquisition and +// resource-ID validation. Retain that ordering even for non-object filters. +func (filter *ActionsWorkflowRunsFilter) UnmarshalJSON(raw []byte) error { + type fields ActionsWorkflowRunsFilter + *filter = ActionsWorkflowRunsFilter{} + message, err := decodeActionsFilter(raw, "workflow_runs_filter", (*fields)(filter)) + filter.validationError = message + return err +} + +func (filter *ActionsWorkflowJobsFilter) UnmarshalJSON(raw []byte) error { + type fields ActionsWorkflowJobsFilter + *filter = ActionsWorkflowJobsFilter{} + message, err := decodeActionsFilter(raw, "workflow_jobs_filter", (*fields)(filter)) + filter.validationError = message + return err +} + +func decodeActionsFilter(raw []byte, field string, output any) (string, error) { + var value any + if err := json.Unmarshal(raw, &value); err != nil { + return "", err + } + if _, err := OptionalParam[map[string]any](map[string]any{field: value}, field); err != nil { + return err.Error(), nil + } + return "", json.Unmarshal(raw, output) +} + +func actionsInputSchema(tool inventory.ServerTool) inventory.ServerTool { + schema := tool.Tool.InputSchema.(*jsonschema.Schema) + // These enums were descriptive on the raw registration path. Keep the + // supported values discoverable without replacing legacy handler errors. + for _, field := range []string{"method", "workflow_runs_filter", "workflow_jobs_filter"} { + property := schema.Properties[field] + if property == nil { + continue + } + if field == "method" { + describeActionsEnum(property) + continue + } + for _, nested := range property.Properties { + describeActionsEnum(nested) + } + schema.Properties[field] = &jsonschema.Schema{ + Description: property.Description, + AnyOf: []*jsonschema.Schema{ + property, + {Not: &jsonschema.Schema{Type: "object"}}, + }, + } + } + return tool +} + +func describeActionsEnum(schema *jsonschema.Schema) { + if len(schema.Enum) == 0 { + return + } + values := make([]string, 0, len(schema.Enum)) + for _, value := range schema.Enum { + values = append(values, fmt.Sprint(value)) + } + schema.Description += " Supported values: " + strings.Join(values, ", ") + "." + schema.Enum = nil +} + +type ActionsGetInput struct { + Method string `json:"method"` + Owner string `json:"owner"` + Repo string `json:"repo"` + ResourceID string `json:"resource_id"` +} + +type ActionsRunTriggerInput struct { + Method string `json:"method"` + Owner string `json:"owner"` + Repo string `json:"repo"` + WorkflowID string `json:"workflow_id,omitempty"` + Ref string `json:"ref,omitempty"` + RunID int `json:"run_id,omitempty"` + Inputs map[string]any `json:"inputs,omitempty"` +} + +type ActionsGetJobLogsInput struct { + Owner string `json:"owner"` + Repo string `json:"repo"` + JobID int `json:"job_id,omitempty"` + RunID int `json:"run_id,omitempty"` + FailedOnly bool `json:"failed_only,omitempty"` + ReturnContent bool `json:"return_content,omitempty"` + TailLines int `json:"tail_lines,omitempty"` +} + +type ActionsListOutput struct { + Workflows *github.Workflows + Runs *MinimalWorkflowRunsResult + Jobs *ActionsJobsOutput + Artifacts *github.ArtifactList +} + +type ActionsJobsOutput struct { + Jobs MinimalWorkflowJobsResult `json:"jobs"` +} + +func (out ActionsListOutput) MarshalJSON() ([]byte, error) { + switch { + case out.Workflows != nil: + return json.Marshal(out.Workflows) + case out.Runs != nil: + return json.Marshal(out.Runs) + case out.Jobs != nil: + return json.Marshal(out.Jobs) + default: + return json.Marshal(out.Artifacts) + } +} + +func actionsListOutputSchema() *jsonschema.Schema { + return actionsUnionSchema( + &jsonschema.Schema{Type: "null"}, + repositoryOutputSchema[github.Workflows](), + repositoryOutputSchema[MinimalWorkflowRunsResult](), + repositoryOutputSchema[ActionsJobsOutput](), + repositoryOutputSchema[github.ArtifactList](), + ) +} + +type ActionsGetOutput struct { + Workflow *github.Workflow + Run *MinimalWorkflowRun + Job *github.WorkflowJob + Usage *github.WorkflowRunUsage + Artifact *ActionsArtifactDownloadOutput + Logs *ActionsRunLogsOutput +} + +type ActionsArtifactDownloadOutput struct { + ArtifactID int64 `json:"artifact_id"` + DownloadURL string `json:"download_url"` + Message string `json:"message"` + Note string `json:"note"` +} + +type ActionsRunLogsOutput struct { + LogsURL string `json:"logs_url"` + Message string `json:"message"` + Note string `json:"note"` + OptimizationTip string `json:"optimization_tip"` + Warning string `json:"warning"` +} + +func (out ActionsGetOutput) MarshalJSON() ([]byte, error) { + switch { + case out.Workflow != nil: + return json.Marshal(out.Workflow) + case out.Run != nil: + return json.Marshal(out.Run) + case out.Job != nil: + return json.Marshal(out.Job) + case out.Usage != nil: + return json.Marshal(out.Usage) + case out.Artifact != nil: + return json.Marshal(out.Artifact) + default: + return json.Marshal(out.Logs) + } +} + +func actionsGetOutputSchema() *jsonschema.Schema { + return actionsUnionSchema( + &jsonschema.Schema{Type: "null"}, + repositoryOutputSchema[github.Workflow](), + repositoryOutputSchema[MinimalWorkflowRun](), + repositoryOutputSchema[github.WorkflowJob](), + repositoryOutputSchema[github.WorkflowRunUsage](), + repositoryOutputSchema[ActionsArtifactDownloadOutput](), + repositoryOutputSchema[ActionsRunLogsOutput](), + ) +} + +type ActionsRunTriggerOutput struct { + Dispatch *ActionsDispatchOutput + Run *ActionsRunOperationOutput +} + +// Workflow dispatch inputs are arbitrary user JSON, echoed verbatim by the +// legacy response. This is the only open-ended output field. +type ActionsDispatchOutput struct { + Inputs json.RawMessage `json:"inputs"` + Message string `json:"message"` + Ref string `json:"ref"` + Status string `json:"status"` + StatusCode int `json:"status_code"` + WorkflowID string `json:"workflow_id"` + WorkflowType string `json:"workflow_type"` +} + +type ActionsRunOperationOutput struct { + Message string `json:"message"` + RunID int64 `json:"run_id"` + Status string `json:"status"` + StatusCode int `json:"status_code"` +} + +func (out ActionsRunTriggerOutput) MarshalJSON() ([]byte, error) { + if out.Dispatch != nil { + return json.Marshal(out.Dispatch) + } + return json.Marshal(out.Run) +} + +func actionsRunTriggerOutputSchema() *jsonschema.Schema { + dispatch := repositoryOutputSchema[ActionsDispatchOutput]() + dispatch.Properties["inputs"] = &jsonschema.Schema{ + Types: []string{"object", "null"}, + AdditionalProperties: &jsonschema.Schema{}, + } + return repositoryUnionSchema( + &jsonschema.Schema{Type: "null"}, + dispatch, + repositoryOutputSchema[ActionsRunOperationOutput](), + ) +} + +// Sparse API objects can satisfy more than one method shape (including {}). +// anyOf preserves those legitimate responses without weakening field types. +func actionsUnionSchema(variants ...*jsonschema.Schema) *jsonschema.Schema { + schema := repositoryUnionSchema(variants...) + schema.AnyOf, schema.OneOf = schema.OneOf, nil + return schema +} + +type ActionsJobLogsOutput struct { + Single *ActionsJobLog + Failed *ActionsFailedJobLogsOutput +} + +type ActionsJobLog struct { + Content *ActionsJobLogContent + URL *ActionsJobLogURL + Error *ActionsJobLogError +} + +type ActionsJobLogContent struct { + JobID int64 `json:"job_id"` + JobName string `json:"job_name,omitempty"` + LogsContent string `json:"logs_content"` + Message string `json:"message"` + OriginalLength int `json:"original_length"` +} + +type ActionsJobLogURL struct { + JobID int64 `json:"job_id"` + JobName string `json:"job_name,omitempty"` + LogsURL string `json:"logs_url"` + Message string `json:"message"` + Note string `json:"note"` +} + +type ActionsJobLogError struct { + Error string `json:"error"` + JobID int64 `json:"job_id"` + JobName string `json:"job_name"` +} + +func (out ActionsJobLog) MarshalJSON() ([]byte, error) { + switch { + case out.Content != nil: + return json.Marshal(out.Content) + case out.URL != nil: + return json.Marshal(out.URL) + default: + return json.Marshal(out.Error) + } +} + +type ActionsFailedJobLogsOutput struct { + FailedJobs int `json:"failed_jobs"` + Logs *[]ActionsJobLog `json:"logs,omitempty"` + Message string `json:"message"` + ReturnFormat *ActionsLogsReturnFormat `json:"return_format,omitempty"` + RunID int64 `json:"run_id"` + TotalJobs int `json:"total_jobs"` +} + +type ActionsLogsReturnFormat struct { + Content bool `json:"content"` + URLs bool `json:"urls"` +} + +func (out ActionsJobLogsOutput) MarshalJSON() ([]byte, error) { + if out.Single != nil { + return json.Marshal(out.Single) + } + return json.Marshal(out.Failed) +} + +func actionsJobLogSchema() *jsonschema.Schema { + return repositoryUnionSchema( + repositoryOutputSchema[ActionsJobLogContent](), + repositoryOutputSchema[ActionsJobLogURL](), + repositoryOutputSchema[ActionsJobLogError](), + ) +} + +func actionsJobLogsOutputSchema() *jsonschema.Schema { + failed := repositoryOutputSchema[ActionsFailedJobLogsOutput]() + failed.Properties["logs"].Items = actionsJobLogSchema() + return repositoryUnionSchema( + &jsonschema.Schema{Type: "null"}, + repositoryOutputSchema[ActionsJobLogContent](), + repositoryOutputSchema[ActionsJobLogURL](), + failed, + ) +} + +// Normalize only fields inspected by each legacy method. Ignored optional +// trigger values and non-string filter values retain their legacy defaults. +func normalizeActionsArguments(kind string) inventory.InputNormalizer { + return func(raw json.RawMessage) (json.RawMessage, error) { + var args map[string]any + if err := json.Unmarshal(raw, &args); err != nil { + return nil, &inventory.ToolInputError{Message: "invalid arguments: " + err.Error()} + } + if args == nil { + return nil, &inventory.ToolInputError{Message: "invalid arguments: arguments must be a JSON object"} + } + if err := normalizeActionsFields(args, kind); err != nil { + return nil, &inventory.ToolInputError{Message: err.Error()} + } + return json.Marshal(args) + } +} + +func normalizeActionsFields(args map[string]any, kind string) error { + for _, field := range []string{"owner", "repo"} { + if _, err := RequiredParam[string](args, field); err != nil { + return err + } + } + if kind != "logs" { + if _, err := RequiredParam[string](args, "method"); err != nil { + return err + } + } + switch kind { + case "get": + _, err := RequiredParam[string](args, "resource_id") + return err + case "list": + if _, err := OptionalParam[string](args, "resource_id"); err != nil { + return err + } + pagination, err := OptionalPaginationParams(args) + if err != nil { + return err + } + args["page"], args["perPage"] = pagination.Page, pagination.PerPage + method := args["method"] + for _, field := range []string{"workflow_runs_filter", "workflow_jobs_filter"} { + relevant := field == "workflow_runs_filter" && method == actionsMethodListWorkflowRuns || + field == "workflow_jobs_filter" && method == actionsMethodListWorkflowJobs + if !relevant { + delete(args, field) + continue + } + filter, err := OptionalParam[map[string]any](args, field) + if err != nil { + // The typed filter retains the error until its method runs. + continue + } + if filter == nil { + delete(args, field) + continue + } + for key, value := range filter { + if _, ok := value.(string); !ok { + filter[key] = "" + } + } + } + case "trigger": + for _, field := range []string{"workflow_id", "ref"} { + if _, ok := args[field].(string); !ok { + delete(args, field) + } + } + runID, _ := OptionalIntParam(args, "run_id") + args["run_id"] = runID + _, err := OptionalParam[map[string]any](args, "inputs") + return err + case "logs": + for _, field := range []string{"job_id", "run_id"} { + value, err := OptionalIntParam(args, field) + if err != nil { + return err + } + args[field] = value + } + for _, field := range []string{"failed_only", "return_content"} { + if _, err := OptionalParam[bool](args, field); err != nil { + return err + } + } + tailLines, err := OptionalIntParam(args, "tail_lines") + if err != nil { + return err + } + if tailLines <= 0 { + tailLines = 500 + } + args["tail_lines"] = tailLines + default: + panic("unknown Actions argument kind: " + kind) + } + return nil +} diff --git a/pkg/github/typed_actions_outputs_test.go b/pkg/github/typed_actions_outputs_test.go new file mode 100644 index 0000000000..13edac599b --- /dev/null +++ b/pkg/github/typed_actions_outputs_test.go @@ -0,0 +1,421 @@ +package github + +import ( + "context" + "encoding/json" + "fmt" + "maps" + "net/http" + "net/http/httptest" + "os" + "reflect" + "strings" + "testing" + "time" + + "github.com/github/github-mcp-server/internal/toolsnaps" + "github.com/github/github-mcp-server/pkg/inventory" + "github.com/github/github-mcp-server/pkg/translations" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func actionsTypedSession(t *testing.T, deps ToolDependencies, protocol string) (*mcp.ClientSession, map[string]*jsonschema.Resolved) { + t.Helper() + tr := translations.NullTranslationHelper + tools := []inventory.ServerTool{ActionsList(tr), ActionsGet(tr), ActionsRunTrigger(tr), ActionsGetJobLogs(tr)} + inv, err := inventory.NewBuilder().SetTools(tools).WithToolsets([]string{"all"}).Build() + require.NoError(t, err) + server := mcp.NewServer(&mcp.Implementation{Name: "actions", Version: "v1"}, nil) + server.AddReceivingMiddleware(InjectDepsMiddleware(deps)) + inv.RegisterTools(context.Background(), server, deps) + if protocol == "" || protocol == "unknown" { + server.AddReceivingMiddleware(func(next mcp.MethodHandler) mcp.MethodHandler { + return func(ctx context.Context, method string, req mcp.Request) (mcp.Result, error) { + switch req := req.(type) { + case *mcp.ListToolsRequest: + req.Params.Meta = mcp.Meta{mcp.MetaKeyProtocolVersion: protocol} + case *mcp.CallToolRequest: + req.Params.Meta = mcp.Meta{mcp.MetaKeyProtocolVersion: protocol} + } + return next(ctx, method, req) + } + }) + } + version := protocol + if version == "" || version == "unknown" { + version = inventory.ProtocolVersionMultiRoundTrip + } + session := connectCommentVisibilityClient(t, server, version) + list, err := session.ListTools(context.Background(), nil) + require.NoError(t, err) + require.Len(t, list.Tools, 4) + schemas := make(map[string]*jsonschema.Resolved) + for _, tool := range list.Tools { + if os.Getenv("ACTIONS_LEGACY_BASELINE") == "true" { + continue + } + if protocol != inventory.ProtocolVersionMultiRoundTrip { + assert.Nil(t, tool.OutputSchema, tool.Name) + continue + } + require.NotNil(t, tool.OutputSchema) + require.NoError(t, toolsnaps.Test(tool.Name+"_typed", *tool)) + var schema jsonschema.Schema + require.NoError(t, json.Unmarshal([]byte(mustMarshalJSON(t, tool.OutputSchema)), &schema)) + resolved, err := schema.Resolve(nil) + require.NoError(t, err) + schemas[tool.Name] = resolved + } + return session, schemas +} + +var typedActionsProtocols = []string{inventory.ProtocolVersionMultiRoundTrip, "2025-11-25", "", "unknown"} + +type actionsWireCase struct { + name, method string + args map[string]any + path, query string + body, text string + status int +} + +func actionsWireCases() []actionsWireCase { + run := `{"id":7,"name":"CI","workflow_id":2,"run_number":3,"run_attempt":1,"status":"completed","conclusion":"failure","head_branch":"main","head_commit":{"message":"change"}}` + return []actionsWireCase{ + {"actions_list", "list_workflows", nil, "/repos/owner/repo/actions/workflows", "page=1&per_page=30", `{"total_count":1,"workflows":[{"id":2,"name":"CI","created_at":"2026-01-01T00:00:00Z"}]}`, `{"total_count":1,"workflows":[{"id":2,"name":"CI","created_at":"2026-01-01T00:00:00Z"}]}`, 200}, + {"actions_list", "list_workflow_runs", map[string]any{"workflow_runs_filter": map[string]any{"actor": "octocat", "branch": false, "event": 1}}, "/repos/owner/repo/actions/runs", "actor=octocat&page=1&per_page=30", `{"total_count":1,"workflow_runs":[` + run + `]}`, `{"total_count":1,"workflow_runs":[` + run + `]}`, 200}, + {"actions_list", "list_workflow_runs", map[string]any{"resource_id": "ci.yml", "page": "2e0", "perPage": "5.0"}, "/repos/owner/repo/actions/workflows/ci.yml/runs", "page=2&per_page=5", `{"total_count":0,"workflow_runs":[]}`, `{"total_count":0,"workflow_runs":[]}`, 200}, + {"actions_list", "list_workflow_runs", map[string]any{"resource_id": "2"}, "/repos/owner/repo/actions/workflows/2/runs", "page=1&per_page=30", `null`, `{"total_count":0,"workflow_runs":[]}`, 200}, + {"actions_list", "list_workflow_jobs", map[string]any{"resource_id": "7", "workflow_jobs_filter": map[string]any{"filter": "all"}}, "/repos/owner/repo/actions/runs/7/jobs", "filter=all&page=1&per_page=30", `{"total_count":1,"jobs":[{"id":8,"run_id":7,"name":"test","status":"completed"}]}`, `{"jobs":{"total_count":1,"jobs":[{"id":8,"run_id":7,"name":"test","status":"completed"}]}}`, 200}, + {"actions_list", "list_workflow_run_artifacts", map[string]any{"resource_id": "7", "workflow_jobs_filter": false}, "/repos/owner/repo/actions/runs/7/artifacts", "page=1&per_page=30", `{"total_count":1,"artifacts":[{"id":9,"digest":"sha256:abc","workflow_run":{"id":7}}]}`, `{"total_count":1,"artifacts":[{"id":9,"digest":"sha256:abc","workflow_run":{"id":7}}]}`, 200}, + {"actions_get", "get_workflow", map[string]any{"resource_id": "ci.yml"}, "/repos/owner/repo/actions/workflows/ci.yml", "", `{"id":2,"name":"CI"}`, `{"id":2,"name":"CI"}`, 200}, + {"actions_get", "get_workflow_run", map[string]any{"resource_id": "7"}, "/repos/owner/repo/actions/runs/7", "", run, run, 200}, + {"actions_get", "get_workflow_job", map[string]any{"resource_id": "8"}, "/repos/owner/repo/actions/jobs/8", "", `{"id":8,"steps":[{"name":"test","number":1,"started_at":"2026-01-01T00:00:00Z"}],"labels":["linux"]}`, `{"id":8,"steps":[{"name":"test","number":1,"started_at":"2026-01-01T00:00:00Z"}],"labels":["linux"]}`, 200}, + {"actions_get", "get_workflow_run_usage", map[string]any{"resource_id": "7"}, "/repos/owner/repo/actions/runs/7/timing", "", `{"billable":{"CUSTOM":{"total_ms":42,"jobs":1,"job_runs":[{"job_id":8,"duration_ms":42}]}},"run_duration_ms":42}`, `{"billable":{"CUSTOM":{"total_ms":42,"jobs":1,"job_runs":[{"job_id":8,"duration_ms":42}]}},"run_duration_ms":42}`, 200}, + {"actions_get", "get_workflow", map[string]any{"resource_id": "-2"}, "/repos/owner/repo/actions/workflows/-2", "", `{}`, `{}`, 200}, + {"actions_get", "get_workflow_job", map[string]any{"resource_id": "8"}, "/repos/owner/repo/actions/jobs/8", "", `null`, `null`, 200}, + {"actions_get", "get_workflow_run_usage", map[string]any{"resource_id": "7"}, "/repos/owner/repo/actions/runs/7/timing", "", `{}`, `{}`, 200}, + {"actions_list", "list_workflows", map[string]any{"page": 0, "perPage": 0, "workflow_runs_filter": nil}, "/repos/owner/repo/actions/workflows", "page=1&per_page=30", `{}`, `{}`, 200}, + {"actions_list", "list_workflow_run_artifacts", map[string]any{"resource_id": "7"}, "/repos/owner/repo/actions/runs/7/artifacts", "page=1&per_page=30", `{}`, `{}`, 200}, + {"actions_list", "list_workflow_runs", map[string]any{"workflow_runs_filter": map[string]any{"event": "custom", "status": "custom"}}, "/repos/owner/repo/actions/runs", "event=custom&page=1&per_page=30&status=custom", `{}`, `{"total_count":0,"workflow_runs":[]}`, 200}, + {"actions_get", "download_workflow_run_artifact", map[string]any{"resource_id": "9"}, "/repos/owner/repo/actions/artifacts/9/zip", "", "", `{"artifact_id":9,"download_url":"https://logs.example/archive","message":"Artifact is available for download","note":"The download_url provides a download link for the artifact as a ZIP archive. The link is temporary and expires after a short time."}`, 302}, + {"actions_get", "get_workflow_run_logs_url", map[string]any{"resource_id": "7"}, "/repos/owner/repo/actions/runs/7/logs", "", "", `{"logs_url":"https://logs.example/archive","message":"Workflow run logs are available for download","note":"The logs_url provides a download link for the complete workflow run logs as a ZIP archive. You can download this archive to extract and examine individual job logs.","optimization_tip":"Use: get_job_logs with parameters {run_id: 7, failed_only: true} for more efficient failed job debugging","warning":"This downloads ALL logs as a ZIP file which can be large and expensive. For debugging failed jobs, consider using get_job_logs with failed_only=true and run_id instead."}`, 302}, + {"actions_run_trigger", "run_workflow", map[string]any{"workflow_id": "ci.yml", "ref": "main"}, "/repos/owner/repo/actions/workflows/ci.yml/dispatches", "", "", `{"inputs":null,"message":"Workflow run has been queued","ref":"main","status":"204 No Content","status_code":204,"workflow_id":"ci.yml","workflow_type":"workflow_file"}`, 204}, + {"actions_run_trigger", "run_workflow", map[string]any{"workflow_id": "2", "ref": "main", "inputs": map[string]any{"flag": true, "nested": []any{nil, 42, map[string]any{"s": "x"}}}}, "/repos/owner/repo/actions/workflows/2/dispatches", "", "", `{"inputs":{"flag":true,"nested":[null,42,{"s":"x"}]},"message":"Workflow run has been queued","ref":"main","status":"204 No Content","status_code":204,"workflow_id":"2","workflow_type":"workflow_id"}`, 204}, + {"actions_run_trigger", "rerun_workflow_run", map[string]any{"run_id": "7.0", "workflow_id": false, "ref": nil}, "/repos/owner/repo/actions/runs/7/rerun", "", "", `{"message":"Workflow run has been queued for re-run","run_id":7,"status":"201 Created","status_code":201}`, 201}, + {"actions_run_trigger", "rerun_failed_jobs", map[string]any{"run_id": "7e0"}, "/repos/owner/repo/actions/runs/7/rerun-failed-jobs", "", "", `{"message":"Failed jobs have been queued for re-run","run_id":7,"status":"201 Created","status_code":201}`, 201}, + {"actions_run_trigger", "cancel_workflow_run", map[string]any{"run_id": 7}, "/repos/owner/repo/actions/runs/7/cancel", "", `{}`, `{"message":"Workflow run has been cancelled","run_id":7,"status":"202 Accepted","status_code":202}`, 202}, + {"actions_run_trigger", "delete_workflow_run_logs", map[string]any{"run_id": 7}, "/repos/owner/repo/actions/runs/7/logs", "", "", `{"message":"Workflow run logs have been deleted","run_id":7,"status":"204 No Content","status_code":204}`, 204}, + } +} + +func actionsCaseArgs(tc actionsWireCase) map[string]any { + args := map[string]any{"owner": "owner", "repo": "repo"} + if tc.method != "" { + args["method"] = tc.method + } + maps.Copy(args, tc.args) + return args +} + +func actionsCaseDeps(t *testing.T, current **actionsWireCase, apiError bool) BaseDeps { + t.Helper() + client := &http.Client{Transport: recorderTransport{handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Host == "logs.example" { + w.Header().Set("Location", "https://logs.example/archive") + w.WriteHeader(http.StatusFound) + return + } + tc := *current + assert.Equal(t, tc.path, r.URL.Path) + assert.Equal(t, tc.query, r.URL.RawQuery) + if apiError { + w.WriteHeader(http.StatusForbidden) + _, _ = w.Write([]byte(`{"message":"Forbidden"}`)) + return + } + if tc.method == "run_workflow" { + var payload map[string]any + require.NoError(t, json.NewDecoder(r.Body).Decode(&payload)) + assert.Equal(t, tc.args["ref"], payload["ref"]) + assert.JSONEq(t, mustMarshalJSON(t, tc.args["inputs"]), mustMarshalJSON(t, payload["inputs"])) + } + if tc.status == 302 { + w.Header().Set("Location", "https://logs.example/archive") + } + w.WriteHeader(tc.status) + _, _ = w.Write([]byte(tc.body)) + })}} + return BaseDeps{Client: mustNewGHClient(t, client), ContentWindowSize: 100, + RepoAccessCache: stubRepoAccessCache(nil, time.Minute)} +} + +func assertActionsWireResult(t *testing.T, result *mcp.CallToolResult, schema *jsonschema.Resolved, text string, isError bool) { + t.Helper() + require.Equal(t, isError, result.IsError, mustMarshalJSON(t, result)) + require.Len(t, result.Content, 1) + assert.Equal(t, text, getTextResult(t, result).Text) + if schema == nil || isError { + assert.Nil(t, result.StructuredContent) + return + } + if text != "null" { + require.NotNil(t, result.StructuredContent) + } + var output any + require.NoError(t, json.Unmarshal([]byte(mustMarshalJSON(t, result.StructuredContent)), &output)) + require.NoError(t, schema.Validate(output)) + assert.JSONEq(t, text, mustMarshalJSON(t, output)) +} + +func TestTypedActionsWireOutputs(t *testing.T) { + for _, protocol := range typedActionsProtocols { + t.Run("protocol="+protocol, func(t *testing.T) { + var current *actionsWireCase + session, schemas := actionsTypedSession(t, actionsCaseDeps(t, ¤t, false), protocol) + for _, tc := range actionsWireCases() { + t.Run(tc.name+"/"+tc.method+"/"+mustMarshalJSON(t, tc.args), func(t *testing.T) { + current = &tc + result, err := session.CallTool(context.Background(), &mcp.CallToolParams{Name: tc.name, Arguments: actionsCaseArgs(tc)}) + require.NoError(t, err) + assertActionsWireResult(t, result, schemas[tc.name], tc.text, false) + }) + } + }) + } +} + +func TestTypedActionsWireErrors(t *testing.T) { + cases := []actionsWireCase{ + {name: "actions_list", args: map[string]any{"owner": "", "method": false}, text: "missing required parameter: owner"}, + {name: "actions_list", method: "list_workflows", args: map[string]any{"resource_id": 7}, text: "parameter resource_id is not of type string, is float64"}, + {name: "actions_list", method: "list_workflows", args: map[string]any{"page": "bad"}, text: "parameter page is not a valid number: invalid numeric value: bad"}, + {name: "actions_list", method: "list_workflow_jobs", text: "missing required parameter for method list_workflow_jobs: resource_id"}, + {name: "actions_list", method: "list_workflow_jobs", args: map[string]any{"resource_id": "bad", "workflow_jobs_filter": false}, text: `invalid resource_id, must be an integer for method list_workflow_jobs: strconv.ParseInt: parsing "bad": invalid syntax`}, + {name: "actions_list", method: "list_workflow_runs", args: map[string]any{"workflow_runs_filter": false}, text: "parameter workflow_runs_filter is not of type map[string]interface {}, is bool"}, + {name: "actions_list", method: "list_workflow_jobs", args: map[string]any{"resource_id": "7", "workflow_jobs_filter": nil}, text: "parameter workflow_jobs_filter is not of type map[string]interface {}, is "}, + {name: "actions_list", method: "list_workflow_jobs", args: map[string]any{"workflow_jobs_filter": false}, text: "missing required parameter for method list_workflow_jobs: resource_id"}, + {name: "actions_list", method: "unknown", args: map[string]any{"resource_id": "7"}, text: "unknown method: unknown"}, + {name: "actions_get", method: "get_workflow", text: "missing required parameter: resource_id"}, + {name: "actions_get", method: "get_workflow_job", args: map[string]any{"resource_id": "7.0"}, text: `invalid resource_id, must be an integer for method get_workflow_job: strconv.ParseInt: parsing "7.0": invalid syntax`}, + {name: "actions_get", method: "unknown", args: map[string]any{"resource_id": "7"}, text: "unknown method: unknown"}, + {name: "actions_run_trigger", method: "run_workflow", args: map[string]any{"workflow_id": false}, text: "workflow_id is required for run_workflow action"}, + {name: "actions_run_trigger", method: "run_workflow", args: map[string]any{"workflow_id": "ci.yml", "ref": false}, text: "ref is required for run_workflow action"}, + {name: "actions_run_trigger", method: "rerun_workflow_run", args: map[string]any{"run_id": "bad"}, text: "missing required parameter: run_id"}, + {name: "actions_run_trigger", method: "run_workflow", args: map[string]any{"inputs": nil}, text: "parameter inputs is not of type map[string]interface {}, is "}, + {name: "get_job_logs", args: map[string]any{"job_id": 1.5}, text: "parameter job_id is not a valid number: non-integer numeric value: 1.5"}, + {name: "get_job_logs", args: map[string]any{"job_id": "8", "failed_only": nil}, text: "parameter failed_only is not of type bool, is "}, + {name: "get_job_logs", args: map[string]any{"failed_only": true}, text: "run_id is required when failed_only is true"}, + {name: "get_job_logs", args: map[string]any{"job_id": -1}, text: "Either job_id must be provided for single job logs, or run_id with failed_only=true for failed job logs"}, + } + + for _, protocol := range typedActionsProtocols { + t.Run("protocol="+protocol, func(t *testing.T) { + var current *actionsWireCase + session, schemas := actionsTypedSession(t, actionsCaseDeps(t, ¤t, false), protocol) + for _, tc := range cases { + t.Run(tc.name+"/"+tc.text, func(t *testing.T) { + result, err := session.CallTool(context.Background(), &mcp.CallToolParams{Name: tc.name, Arguments: actionsCaseArgs(tc)}) + require.NoError(t, err) + assertActionsWireResult(t, result, schemas[tc.name], tc.text, true) + }) + } + session, _ = actionsTypedSession(t, actionsCaseDeps(t, ¤t, true), protocol) + for _, tc := range actionsWireCases() { + current = &tc + result, err := session.CallTool(context.Background(), &mcp.CallToolParams{Name: tc.name, Arguments: actionsCaseArgs(tc)}) + require.NoError(t, err) + require.True(t, result.IsError) + require.Len(t, result.Content, 1) + assert.Contains(t, getTextResult(t, result).Text, "Forbidden") + assert.Nil(t, result.StructuredContent) + } + }) + } +} + +func TestTypedActionsClientErrorPrecedence(t *testing.T) { + for _, protocol := range typedActionsProtocols { + session, _ := actionsTypedSession(t, failingSearchClientDeps{}, protocol) + for _, tc := range []actionsWireCase{ + {name: "actions_list", method: "list_workflow_jobs", args: map[string]any{"resource_id": "bad", "workflow_jobs_filter": false}}, + {name: "actions_list", method: "list_workflow_runs", args: map[string]any{"workflow_runs_filter": nil}}, + {name: "actions_get", method: "unknown", args: map[string]any{"resource_id": "bad"}}, + {name: "get_job_logs", args: map[string]any{"failed_only": true}}, + {name: "actions_run_trigger", method: "unknown", args: map[string]any{"run_id": 7}}, + } { + result, err := session.CallTool(context.Background(), &mcp.CallToolParams{Name: tc.name, Arguments: actionsCaseArgs(tc)}) + if os.Getenv("ACTIONS_LEGACY_BASELINE") == "true" { + require.EqualError(t, err, `calling "tools/call": failed to get GitHub client: client unavailable`) + continue + } + require.NoError(t, err) + assertActionsWireResult(t, result, nil, "failed to get GitHub client: client unavailable", true) + } + } +} + +func TestTypedActionsIFCAndAnnotations(t *testing.T) { + for _, protocol := range typedActionsProtocols { + for _, private := range []bool{false, true} { + for _, enabled := range []bool{false, true} { + for _, tc := range []actionsWireCase{ + {name: "actions_list", method: "list_workflows"}, + {name: "actions_get", method: "get_workflow", args: map[string]any{"resource_id": "ci.yml"}}, + {name: "get_job_logs", args: map[string]any{"run_id": "7e0", "failed_only": true}}, + {name: "actions_run_trigger", method: "delete_workflow_run_logs", args: map[string]any{"run_id": 7}}, + } { + t.Run(fmt.Sprintf("%s/%s/private=%t/enabled=%t", tc.name, protocol, private, enabled), func(t *testing.T) { + client := &http.Client{Transport: recorderTransport{handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/repos/owner/repo" { + _, _ = fmt.Fprintf(w, `{"private":%t}`, private) + return + } + if r.Method == http.MethodDelete { + w.WriteHeader(http.StatusNoContent) + return + } + _, _ = w.Write([]byte(`{}`)) + })}} + deps := BaseDeps{Client: mustNewGHClient(t, client), + RepoAccessCache: stubRepoAccessCache(nil, time.Minute)} + if enabled { + deps.featureChecker = featureCheckerFor(FeatureFlagIFCLabels) + } + session, schemas := actionsTypedSession(t, deps, protocol) + listed, err := session.ListTools(context.Background(), nil) + require.NoError(t, err) + for _, tool := range listed.Tools { + require.NotNil(t, tool.Annotations) + assert.Equal(t, tool.Name != "actions_run_trigger", tool.Annotations.ReadOnlyHint) + if tool.Name == "actions_run_trigger" { + require.NotNil(t, tool.Annotations.DestructiveHint) + assert.True(t, *tool.Annotations.DestructiveHint) + } + } + result, err := session.CallTool(context.Background(), &mcp.CallToolParams{Name: tc.name, Arguments: actionsCaseArgs(tc)}) + require.NoError(t, err) + assertActionsWireResult(t, result, schemas[tc.name], getTextResult(t, result).Text, false) + if !enabled || tc.name == "actions_run_trigger" { + assert.Nil(t, result.Meta["ifc"]) + return + } + label := unmarshalIFC(t, result.Meta["ifc"]) + assert.Equal(t, "untrusted", label["integrity"]) + confidentiality := "public" + if private { + confidentiality = "private" + } + assert.Equal(t, confidentiality, label["confidentiality"]) + }) + } + } + } + } +} + +func TestActionsConcreteSchemas(t *testing.T) { + for name, schema := range map[string]*jsonschema.Schema{ + "list": actionsListOutputSchema(), "get": actionsGetOutputSchema(), + "trigger": actionsRunTriggerOutputSchema(), "logs": actionsJobLogsOutputSchema(), + } { + t.Run(name, func(t *testing.T) { + resolved, err := schema.Resolve(nil) + require.NoError(t, err) + for _, raw := range []string{`[]`, `42`, `{"unknown":true}`, `{"id":"wrong"}`, `{"jobs":"wrong"}`, `{"logs_url":false}`} { + var invalid any + require.NoError(t, json.Unmarshal([]byte(raw), &invalid)) + require.Error(t, resolved.Validate(invalid), raw) + } + }) + } + seen := make(map[reflect.Type]bool) + var check func(reflect.Type) + check = func(typ reflect.Type) { + if seen[typ] || typ == reflect.TypeFor[json.RawMessage]() { + return + } + seen[typ] = true + switch typ.Kind() { + case reflect.Interface: + t.Errorf("untyped Actions output: %s", typ) + case reflect.Array, reflect.Slice, reflect.Pointer, reflect.Map: + check(typ.Elem()) + case reflect.Struct: + for field := range typ.Fields() { + if field.IsExported() { + check(field.Type) + } + } + } + } + check(reflect.TypeFor[ActionsListOutput]()) + check(reflect.TypeFor[ActionsGetOutput]()) + check(reflect.TypeFor[ActionsRunTriggerOutput]()) + check(reflect.TypeFor[ActionsJobLogsOutput]()) +} +func TestTypedActionsJobLogWireOutputs(t *testing.T) { + logServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + _, _ = w.Write([]byte("first\nsecond\nthird")) + })) + defer logServer.Close() + const note = "The logs_url provides a download link for the individual job logs in plain text format. Use return_content=true to get the actual log content." + for _, protocol := range typedActionsProtocols { + for _, outcome := range []string{"single", "failed", "partial", "none"} { + for _, content := range []bool{false, true} { + t.Run(fmt.Sprintf("%s/%s/content=%t", protocol, outcome, content), func(t *testing.T) { + client := &http.Client{Transport: recorderTransport{handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case strings.HasSuffix(r.URL.Path, "/jobs"): + body := `{"total_count":2,"jobs":[{"id":8,"name":"test","conclusion":"failure"},{"id":9,"name":"build","conclusion":"failure"}]}` + if outcome == "none" { + body = `{"total_count":1,"jobs":[{"id":8,"conclusion":"success"}]}` + } + _, _ = w.Write([]byte(body)) + case outcome == "partial" && strings.HasSuffix(r.URL.Path, "/9/logs"): + w.WriteHeader(403) + _, _ = w.Write([]byte(`{"message":"Forbidden"}`)) + default: + w.Header().Set("Location", logServer.URL) + w.WriteHeader(302) + } + })}} + deps := BaseDeps{Client: mustNewGHClient(t, client), ContentWindowSize: 100, + RepoAccessCache: stubRepoAccessCache(nil, time.Minute)} + session, schemas := actionsTypedSession(t, deps, protocol) + args := map[string]any{"owner": "owner", "repo": "repo", "return_content": content, "tail_lines": "2.0"} + if outcome == "single" { + args["job_id"] = "8e0" + } else { + args["run_id"], args["failed_only"] = "7", true + } + result, err := session.CallTool(context.Background(), &mcp.CallToolParams{Name: "get_job_logs", Arguments: args}) + require.NoError(t, err) + text := getTextResult(t, result).Text + assertActionsWireResult(t, result, schemas["get_job_logs"], text, false) + var payload map[string]any + require.NoError(t, json.Unmarshal([]byte(text), &payload)) + switch { + case outcome == "none": + require.Equal(t, `{"failed_jobs":0,"message":"No failed jobs found in this workflow run","run_id":7,"total_jobs":1}`, text) + case outcome == "single" && content: + require.Equal(t, `{"job_id":8,"logs_content":"second\nthird","message":"Job logs content retrieved successfully","original_length":3}`, text) + case outcome == "single": + require.Equal(t, fmt.Sprintf(`{"job_id":8,"logs_url":%q,"message":"Job logs are available for download","note":%q}`, logServer.URL, note), text) + default: + logs := payload["logs"].([]any) + require.Len(t, logs, 2) + if outcome == "partial" { + assert.Contains(t, logs[1].(map[string]any)["error"], "Forbidden") + } + require.Equal(t, float64(2), payload["failed_jobs"]) + require.Equal(t, "test", logs[0].(map[string]any)["job_name"]) + } + }) + } + } + } +} diff --git a/pkg/inventory/typed_output.go b/pkg/inventory/typed_output.go index 6aa8f4afe3..22b6ea5e61 100644 --- a/pkg/inventory/typed_output.go +++ b/pkg/inventory/typed_output.go @@ -111,7 +111,7 @@ func typedOutputMiddleware(normalizerByName map[string]InputNormalizer) mcp.Midd toolCopy.Meta = maps.Clone(tool.Meta) delete(toolCopy.Meta, typedOutputMetaKey) } - if !protocolVersionAllowed(protocolVersion, ProtocolVersionMultiRoundTrip) { + if !typedOutputProtocolSupported(protocolVersion) { toolCopy.OutputSchema = nil } tools[i] = &toolCopy @@ -139,7 +139,7 @@ func typedOutputMiddleware(normalizerByName map[string]InputNormalizer) mcp.Midd // its serialized typed output, including an error zero. resultCopy.StructuredContent = metadata.explicitOutput } - if (!metadata.hasOutput && metadata.explicitOutput == nil) || !protocolVersionAllowed(requestProtocolVersion(ctx, req.ProtocolVersion()), ProtocolVersionMultiRoundTrip) { + if (!metadata.hasOutput && metadata.explicitOutput == nil) || !typedOutputProtocolSupported(requestProtocolVersion(ctx, req.ProtocolVersion())) { resultCopy.StructuredContent = nil } return &resultCopy, nil @@ -150,6 +150,11 @@ func typedOutputMiddleware(normalizerByName map[string]InputNormalizer) mcp.Midd } } +func typedOutputProtocolSupported(version string) bool { + // Unknown versions must not acquire capabilities through lexical ordering. + return version == ProtocolVersionMultiRoundTrip +} + func removeTypedOutputFallback(result *mcp.CallToolResult, metadata typedOutputMetadata) error { if result.StructuredContent == nil { return nil diff --git a/pkg/inventory/typed_output_test.go b/pkg/inventory/typed_output_test.go index d7fae002f3..fe04835db9 100644 --- a/pkg/inventory/typed_output_test.go +++ b/pkg/inventory/typed_output_test.go @@ -866,6 +866,8 @@ func TestTypedOutputProtocolGateUsesStatelessRequestVersion(t *testing.T) { {name: "stateless modern", protocolVersion: ProtocolVersionMultiRoundTrip, wantSchema: true, wantStructured: true}, {name: "stateless legacy", protocolVersion: "2025-11-25"}, {name: "unknown version defaults to legacy behavior"}, + {name: "unrecognized token defaults to legacy behavior", protocolVersion: "unknown"}, + {name: "unrecognized future date defaults to legacy behavior", protocolVersion: "2099-01-01"}, } { t.Run(tc.name, func(t *testing.T) { meta := mcp.Meta{} diff --git a/third-party-licenses.darwin.md b/third-party-licenses.darwin.md index 817fb7b2a9..d67b374b56 100644 --- a/third-party-licenses.darwin.md +++ b/third-party-licenses.darwin.md @@ -21,7 +21,7 @@ The following packages are included for the amd64, arm64 architectures. - [github.com/google/go-querystring/query](https://pkg.go.dev/github.com/google/go-querystring/query) ([BSD-3-Clause](https://github.com/google/go-querystring/blob/v1.2.0/LICENSE)) - [github.com/google/jsonschema-go/jsonschema](https://pkg.go.dev/github.com/google/jsonschema-go/jsonschema) ([MIT](https://github.com/google/jsonschema-go/blob/v0.4.3/LICENSE)) - [github.com/gorilla/css/scanner](https://pkg.go.dev/github.com/gorilla/css/scanner) ([BSD-3-Clause](https://github.com/gorilla/css/blob/v1.0.1/LICENSE)) - - [github.com/josephburnett/jd/v2](https://pkg.go.dev/github.com/josephburnett/jd/v2) ([MIT](https://github.com/josephburnett/jd/blob/v2.5.0/LICENSE)) + - [github.com/josephburnett/jd/v2](https://pkg.go.dev/github.com/josephburnett/jd/v2) ([MIT](https://github.com/josephburnett/jd/blob/v2.5.0/v2/LICENSE)) - [github.com/lithammer/fuzzysearch/fuzzy](https://pkg.go.dev/github.com/lithammer/fuzzysearch/fuzzy) ([MIT](https://github.com/lithammer/fuzzysearch/blob/v1.1.8/LICENSE)) - [github.com/microcosm-cc/bluemonday](https://pkg.go.dev/github.com/microcosm-cc/bluemonday) ([BSD-3-Clause](https://github.com/microcosm-cc/bluemonday/blob/v1.0.27/LICENSE.md)) - [github.com/modelcontextprotocol/go-sdk](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk) ([Apache-2.0](https://github.com/modelcontextprotocol/go-sdk/blob/v1.8.0/LICENSE)) diff --git a/third-party-licenses.linux.md b/third-party-licenses.linux.md index ae5e60d8ea..a1abb6821b 100644 --- a/third-party-licenses.linux.md +++ b/third-party-licenses.linux.md @@ -21,7 +21,7 @@ The following packages are included for the 386, amd64, arm64 architectures. - [github.com/google/go-querystring/query](https://pkg.go.dev/github.com/google/go-querystring/query) ([BSD-3-Clause](https://github.com/google/go-querystring/blob/v1.2.0/LICENSE)) - [github.com/google/jsonschema-go/jsonschema](https://pkg.go.dev/github.com/google/jsonschema-go/jsonschema) ([MIT](https://github.com/google/jsonschema-go/blob/v0.4.3/LICENSE)) - [github.com/gorilla/css/scanner](https://pkg.go.dev/github.com/gorilla/css/scanner) ([BSD-3-Clause](https://github.com/gorilla/css/blob/v1.0.1/LICENSE)) - - [github.com/josephburnett/jd/v2](https://pkg.go.dev/github.com/josephburnett/jd/v2) ([MIT](https://github.com/josephburnett/jd/blob/v2.5.0/LICENSE)) + - [github.com/josephburnett/jd/v2](https://pkg.go.dev/github.com/josephburnett/jd/v2) ([MIT](https://github.com/josephburnett/jd/blob/v2.5.0/v2/LICENSE)) - [github.com/lithammer/fuzzysearch/fuzzy](https://pkg.go.dev/github.com/lithammer/fuzzysearch/fuzzy) ([MIT](https://github.com/lithammer/fuzzysearch/blob/v1.1.8/LICENSE)) - [github.com/microcosm-cc/bluemonday](https://pkg.go.dev/github.com/microcosm-cc/bluemonday) ([BSD-3-Clause](https://github.com/microcosm-cc/bluemonday/blob/v1.0.27/LICENSE.md)) - [github.com/modelcontextprotocol/go-sdk](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk) ([Apache-2.0](https://github.com/modelcontextprotocol/go-sdk/blob/v1.8.0/LICENSE)) diff --git a/third-party-licenses.windows.md b/third-party-licenses.windows.md index adab52cdee..7e6f57aeb7 100644 --- a/third-party-licenses.windows.md +++ b/third-party-licenses.windows.md @@ -22,7 +22,7 @@ The following packages are included for the 386, amd64, arm64 architectures. - [github.com/google/jsonschema-go/jsonschema](https://pkg.go.dev/github.com/google/jsonschema-go/jsonschema) ([MIT](https://github.com/google/jsonschema-go/blob/v0.4.3/LICENSE)) - [github.com/gorilla/css/scanner](https://pkg.go.dev/github.com/gorilla/css/scanner) ([BSD-3-Clause](https://github.com/gorilla/css/blob/v1.0.1/LICENSE)) - [github.com/inconshreveable/mousetrap](https://pkg.go.dev/github.com/inconshreveable/mousetrap) ([Apache-2.0](https://github.com/inconshreveable/mousetrap/blob/v1.1.0/LICENSE)) - - [github.com/josephburnett/jd/v2](https://pkg.go.dev/github.com/josephburnett/jd/v2) ([MIT](https://github.com/josephburnett/jd/blob/v2.5.0/LICENSE)) + - [github.com/josephburnett/jd/v2](https://pkg.go.dev/github.com/josephburnett/jd/v2) ([MIT](https://github.com/josephburnett/jd/blob/v2.5.0/v2/LICENSE)) - [github.com/lithammer/fuzzysearch/fuzzy](https://pkg.go.dev/github.com/lithammer/fuzzysearch/fuzzy) ([MIT](https://github.com/lithammer/fuzzysearch/blob/v1.1.8/LICENSE)) - [github.com/microcosm-cc/bluemonday](https://pkg.go.dev/github.com/microcosm-cc/bluemonday) ([BSD-3-Clause](https://github.com/microcosm-cc/bluemonday/blob/v1.0.27/LICENSE.md)) - [github.com/modelcontextprotocol/go-sdk](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk) ([Apache-2.0](https://github.com/modelcontextprotocol/go-sdk/blob/v1.8.0/LICENSE))