Skip to content

Commit 9d3173f

Browse files
Merge branch 'main' into raw-client-error-annotation
2 parents a1b6d7b + 63c7db0 commit 9d3173f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+8778
-6471
lines changed

README.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,40 @@ The following sets of tools are available:
490490

491491
<summary><picture><source media="(prefers-color-scheme: dark)" srcset="pkg/octicons/icons/workflow-dark.png"><source media="(prefers-color-scheme: light)" srcset="pkg/octicons/icons/workflow-light.png"><img src="pkg/octicons/icons/workflow-light.png" width="20" height="20" alt="workflow"></picture> Actions</summary>
492492

493+
- **actions_get** - Get details of GitHub Actions resources (workflows, workflow runs, jobs, and artifacts)
494+
- `method`: The method to execute (string, required)
495+
- `owner`: Repository owner (string, required)
496+
- `repo`: Repository name (string, required)
497+
- `resource_id`: The unique identifier of the resource. This will vary based on the "method" provided, so ensure you provide the correct ID:
498+
- Provide a workflow ID or workflow file name (e.g. ci.yaml) for 'get_workflow' method.
499+
- Provide a workflow run ID for 'get_workflow_run', 'get_workflow_run_usage', and 'get_workflow_run_logs_url' methods.
500+
- Provide an artifact ID for 'download_workflow_run_artifact' method.
501+
- Provide a job ID for 'get_workflow_job' method.
502+
(string, required)
503+
504+
- **actions_list** - List GitHub Actions workflows in a repository
505+
- `method`: The action to perform (string, required)
506+
- `owner`: Repository owner (string, required)
507+
- `page`: Page number for pagination (default: 1) (number, optional)
508+
- `per_page`: Results per page for pagination (default: 30, max: 100) (number, optional)
509+
- `repo`: Repository name (string, required)
510+
- `resource_id`: The unique identifier of the resource. This will vary based on the "method" provided, so ensure you provide the correct ID:
511+
- Do not provide any resource ID for 'list_workflows' method.
512+
- Provide a workflow ID or workflow file name (e.g. ci.yaml) for 'list_workflow_runs' method.
513+
- Provide a workflow run ID for 'list_workflow_jobs' and 'list_workflow_run_artifacts' methods.
514+
(string, optional)
515+
- `workflow_jobs_filter`: Filters for workflow jobs. **ONLY** used when method is 'list_workflow_jobs' (object, optional)
516+
- `workflow_runs_filter`: Filters for workflow runs. **ONLY** used when method is 'list_workflow_runs' (object, optional)
517+
518+
- **actions_run_trigger** - Trigger GitHub Actions workflow actions
519+
- `inputs`: Inputs the workflow accepts. Only used for 'run_workflow' method. (object, optional)
520+
- `method`: The method to execute (string, required)
521+
- `owner`: Repository owner (string, required)
522+
- `ref`: The git reference for the workflow. The reference can be a branch or tag name. Required for 'run_workflow' method. (string, optional)
523+
- `repo`: Repository name (string, required)
524+
- `run_id`: The ID of the workflow run. Required for all methods except 'run_workflow'. (number, optional)
525+
- `workflow_id`: The workflow ID (numeric) or workflow file name (e.g., main.yml, ci.yaml). Required for 'run_workflow' method. (string, optional)
526+
493527
- **cancel_workflow_run** - Cancel workflow run
494528
- `owner`: Repository owner (string, required)
495529
- `repo`: Repository name (string, required)
@@ -514,6 +548,15 @@ The following sets of tools are available:
514548
- `run_id`: Workflow run ID (required when using failed_only) (number, optional)
515549
- `tail_lines`: Number of lines to return from the end of the log (number, optional)
516550

551+
- **get_job_logs** - Get GitHub Actions workflow job logs
552+
- `failed_only`: When true, gets logs for all failed jobs in the workflow run specified by run_id. Requires run_id to be provided. (boolean, optional)
553+
- `job_id`: The unique identifier of the workflow job. Required when getting logs for a single job. (number, optional)
554+
- `owner`: Repository owner (string, required)
555+
- `repo`: Repository name (string, required)
556+
- `return_content`: Returns actual log content instead of URLs (boolean, optional)
557+
- `run_id`: The unique identifier of the workflow run. Required when failed_only is true to get logs for all failed jobs in the run. (number, optional)
558+
- `tail_lines`: Number of lines to return from the end of the log (number, optional)
559+
517560
- **get_workflow_run** - Get workflow run
518561
- `owner`: Repository owner (string, required)
519562
- `repo`: Repository name (string, required)

cmd/github-mcp-server/main.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,18 @@ var (
5454

5555
// Parse tools (similar to toolsets)
5656
var enabledTools []string
57-
if err := viper.UnmarshalKey("tools", &enabledTools); err != nil {
58-
return fmt.Errorf("failed to unmarshal tools: %w", err)
57+
if viper.IsSet("tools") {
58+
if err := viper.UnmarshalKey("tools", &enabledTools); err != nil {
59+
return fmt.Errorf("failed to unmarshal tools: %w", err)
60+
}
5961
}
6062

6163
// Parse enabled features (similar to toolsets)
6264
var enabledFeatures []string
63-
if err := viper.UnmarshalKey("features", &enabledFeatures); err != nil {
64-
return fmt.Errorf("failed to unmarshal features: %w", err)
65+
if viper.IsSet("features") {
66+
if err := viper.UnmarshalKey("features", &enabledFeatures); err != nil {
67+
return fmt.Errorf("failed to unmarshal features: %w", err)
68+
}
6569
}
6670

6771
ttl := viper.GetDuration("repo-access-cache-ttl")

docs/tool-renaming.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,5 +46,20 @@ Will get `issue_read` and `get_file_contents` tools registered, with no errors.
4646
<!-- START AUTOMATED ALIASES -->
4747
| Old Name | New Name |
4848
|----------|----------|
49-
| *(none currently)* | |
49+
| `cancel_workflow_run` | `actions_run_trigger` |
50+
| `delete_workflow_run_logs` | `actions_run_trigger` |
51+
| `download_workflow_run_artifact` | `actions_get` |
52+
| `get_workflow` | `actions_get` |
53+
| `get_workflow_job` | `actions_get` |
54+
| `get_workflow_job_logs` | `actions_get` |
55+
| `get_workflow_run` | `actions_get` |
56+
| `get_workflow_run_logs` | `actions_get` |
57+
| `get_workflow_run_usage` | `actions_get` |
58+
| `list_workflow_jobs` | `actions_list` |
59+
| `list_workflow_run_artifacts` | `actions_list` |
60+
| `list_workflow_runs` | `actions_list` |
61+
| `list_workflows` | `actions_list` |
62+
| `rerun_failed_jobs` | `actions_run_trigger` |
63+
| `rerun_workflow_run` | `actions_run_trigger` |
64+
| `run_workflow` | `actions_run_trigger` |
5065
<!-- END AUTOMATED ALIASES -->

internal/ghmcp/server.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,13 @@ func NewMCPServer(cfg MCPServerConfig) (*mcp.Server, error) {
203203
cfg.ContentWindowSize,
204204
)
205205

206+
// Inject dependencies into context for all tool handlers
207+
ghServer.AddReceivingMiddleware(func(next mcp.MethodHandler) mcp.MethodHandler {
208+
return func(ctx context.Context, method string, req mcp.Request) (mcp.Result, error) {
209+
return next(github.ContextWithDeps(ctx, deps), method, req)
210+
}
211+
})
212+
206213
// Build and register the tool/resource/prompt inventory
207214
inventory := github.NewInventory(cfg.Translator).
208215
WithDeprecatedAliases(github.DeprecatedToolAliases).

internal/ghmcp/server_test.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package ghmcp
2+
3+
import (
4+
"testing"
5+
6+
"github.com/github/github-mcp-server/pkg/translations"
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
// TestNewMCPServer_CreatesSuccessfully verifies that the server can be created
12+
// with the deps injection middleware properly configured.
13+
func TestNewMCPServer_CreatesSuccessfully(t *testing.T) {
14+
t.Parallel()
15+
16+
// Create a minimal server configuration
17+
cfg := MCPServerConfig{
18+
Version: "test",
19+
Host: "", // defaults to github.com
20+
Token: "test-token",
21+
EnabledToolsets: []string{"context"},
22+
ReadOnly: false,
23+
Translator: translations.NullTranslationHelper,
24+
ContentWindowSize: 5000,
25+
LockdownMode: false,
26+
}
27+
28+
// Create the server
29+
server, err := NewMCPServer(cfg)
30+
require.NoError(t, err, "expected server creation to succeed")
31+
require.NotNil(t, server, "expected server to be non-nil")
32+
33+
// The fact that the server was created successfully indicates that:
34+
// 1. The deps injection middleware is properly added
35+
// 2. Tools can be registered without panicking
36+
//
37+
// If the middleware wasn't properly added, tool calls would panic with
38+
// "ToolDependencies not found in context" when executed.
39+
//
40+
// The actual middleware functionality and tool execution with ContextWithDeps
41+
// is already tested in pkg/github/*_test.go.
42+
}
43+
44+
// TestResolveEnabledToolsets verifies the toolset resolution logic.
45+
func TestResolveEnabledToolsets(t *testing.T) {
46+
t.Parallel()
47+
48+
tests := []struct {
49+
name string
50+
cfg MCPServerConfig
51+
expectedResult []string
52+
}{
53+
{
54+
name: "nil toolsets without dynamic mode and no tools - use defaults",
55+
cfg: MCPServerConfig{
56+
EnabledToolsets: nil,
57+
DynamicToolsets: false,
58+
EnabledTools: nil,
59+
},
60+
expectedResult: nil, // nil means "use defaults"
61+
},
62+
{
63+
name: "nil toolsets with dynamic mode - start empty",
64+
cfg: MCPServerConfig{
65+
EnabledToolsets: nil,
66+
DynamicToolsets: true,
67+
EnabledTools: nil,
68+
},
69+
expectedResult: []string{}, // empty slice means no toolsets
70+
},
71+
{
72+
name: "explicit toolsets",
73+
cfg: MCPServerConfig{
74+
EnabledToolsets: []string{"repos", "issues"},
75+
DynamicToolsets: false,
76+
},
77+
expectedResult: []string{"repos", "issues"},
78+
},
79+
{
80+
name: "empty toolsets - disable all",
81+
cfg: MCPServerConfig{
82+
EnabledToolsets: []string{},
83+
DynamicToolsets: false,
84+
},
85+
expectedResult: []string{}, // empty slice means no toolsets
86+
},
87+
{
88+
name: "specific tools without toolsets - no default toolsets",
89+
cfg: MCPServerConfig{
90+
EnabledToolsets: nil,
91+
DynamicToolsets: false,
92+
EnabledTools: []string{"get_me"},
93+
},
94+
expectedResult: []string{}, // empty slice when tools specified but no toolsets
95+
},
96+
{
97+
name: "dynamic mode with explicit toolsets removes all and default",
98+
cfg: MCPServerConfig{
99+
EnabledToolsets: []string{"all", "repos"},
100+
DynamicToolsets: true,
101+
},
102+
expectedResult: []string{"repos"}, // "all" is removed in dynamic mode
103+
},
104+
}
105+
106+
for _, tc := range tests {
107+
t.Run(tc.name, func(t *testing.T) {
108+
result := resolveEnabledToolsets(tc.cfg)
109+
assert.Equal(t, tc.expectedResult, result)
110+
})
111+
}
112+
}

pkg/buffer/buffer.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ import (
2626
// The function uses a ring buffer to efficiently store only the last maxJobLogLines lines.
2727
// If the response contains more lines than maxJobLogLines, only the most recent lines are kept.
2828
func ProcessResponseAsRingBufferToEnd(httpResp *http.Response, maxJobLogLines int) (string, int, *http.Response, error) {
29+
if maxJobLogLines > 100000 {
30+
maxJobLogLines = 100000
31+
}
32+
2933
lines := make([]string, maxJobLogLines)
3034
validLines := make([]bool, maxJobLogLines)
3135
totalLines := 0
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
{
2+
"annotations": {
3+
"readOnlyHint": true,
4+
"title": "Get details of GitHub Actions resources (workflows, workflow runs, jobs, and artifacts)"
5+
},
6+
"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",
7+
"inputSchema": {
8+
"type": "object",
9+
"required": [
10+
"method",
11+
"owner",
12+
"repo",
13+
"resource_id"
14+
],
15+
"properties": {
16+
"method": {
17+
"type": "string",
18+
"description": "The method to execute",
19+
"enum": [
20+
"get_workflow",
21+
"get_workflow_run",
22+
"get_workflow_job",
23+
"download_workflow_run_artifact",
24+
"get_workflow_run_usage",
25+
"get_workflow_run_logs_url"
26+
]
27+
},
28+
"owner": {
29+
"type": "string",
30+
"description": "Repository owner"
31+
},
32+
"repo": {
33+
"type": "string",
34+
"description": "Repository name"
35+
},
36+
"resource_id": {
37+
"type": "string",
38+
"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"
39+
}
40+
}
41+
},
42+
"name": "actions_get"
43+
}

0 commit comments

Comments
 (0)