-
Notifications
You must be signed in to change notification settings - Fork 2.5k
Add returning ResourceLink
type from actions logs and file contents
#1004
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
+506
−4
Draft
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
7c67d9c
feat: Add return_resource_links parameter to GetFileContents and GetJ...
mattdholloway d4e03a9
Merge branch 'main' into use-resource-link
mattdholloway 66cdefa
lint: fix if to case
mattdholloway a92c00e
Merge branch 'main' into use-resource-link
mattdholloway File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,195 @@ | ||
package github | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"fmt" | ||
"net/http" | ||
"strconv" | ||
|
||
"github.com/github/github-mcp-server/internal/profiler" | ||
"github.com/github/github-mcp-server/pkg/translations" | ||
"github.com/mark3labs/mcp-go/mcp" | ||
"github.com/mark3labs/mcp-go/server" | ||
) | ||
|
||
// GetWorkflowRunLogsResource defines the resource template and handler for getting workflow run logs. | ||
func GetWorkflowRunLogsResource(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) { | ||
return mcp.NewResourceTemplate( | ||
"actions://{owner}/{repo}/runs/{runId}/logs", // Resource template | ||
t("RESOURCE_WORKFLOW_RUN_LOGS_DESCRIPTION", "Workflow Run Logs"), | ||
), | ||
WorkflowRunLogsResourceHandler(getClient) | ||
} | ||
|
||
// GetJobLogsResource defines the resource template and handler for getting individual job logs. | ||
func GetJobLogsResource(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.ResourceTemplate, server.ResourceTemplateHandlerFunc) { | ||
return mcp.NewResourceTemplate( | ||
"actions://{owner}/{repo}/jobs/{jobId}/logs", // Resource template | ||
t("RESOURCE_JOB_LOGS_DESCRIPTION", "Job Logs"), | ||
), | ||
JobLogsResourceHandler(getClient) | ||
} | ||
|
||
// WorkflowRunLogsResourceHandler returns a handler function for workflow run logs requests. | ||
func WorkflowRunLogsResourceHandler(getClient GetClientFn) func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { | ||
return func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { | ||
// Parse parameters from the URI template matcher | ||
owner, ok := request.Params.Arguments["owner"].([]string) | ||
if !ok || len(owner) == 0 { | ||
return nil, errors.New("owner is required") | ||
} | ||
|
||
repo, ok := request.Params.Arguments["repo"].([]string) | ||
if !ok || len(repo) == 0 { | ||
return nil, errors.New("repo is required") | ||
} | ||
|
||
runIDStr, ok := request.Params.Arguments["runId"].([]string) | ||
if !ok || len(runIDStr) == 0 { | ||
return nil, errors.New("runId is required") | ||
} | ||
|
||
runID, err := strconv.ParseInt(runIDStr[0], 10, 64) | ||
if err != nil { | ||
return nil, fmt.Errorf("invalid runId: %w", err) | ||
} | ||
|
||
client, err := getClient(ctx) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to get GitHub client: %w", err) | ||
} | ||
|
||
// Get the JIT URL for workflow run logs | ||
url, resp, err := client.Actions.GetWorkflowRunLogs(ctx, owner[0], repo[0], runID, 1) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to get workflow run logs URL: %w", err) | ||
} | ||
defer func() { _ = resp.Body.Close() }() | ||
|
||
// Download the logs content immediately using the JIT URL | ||
content, err := downloadLogsFromJITURL(ctx, url.String()) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to download workflow run logs: %w", err) | ||
} | ||
|
||
return []mcp.ResourceContents{ | ||
mcp.TextResourceContents{ | ||
URI: request.Params.URI, | ||
MIMEType: "application/zip", | ||
Text: fmt.Sprintf("Workflow run logs for run %d (ZIP archive)\n\nNote: This is a ZIP archive containing all job logs. Download URL was: %s\n\nContent length: %d bytes", runID, url.String(), len(content)), | ||
}, | ||
}, nil | ||
} | ||
} | ||
|
||
// JobLogsResourceHandler returns a handler function for individual job logs requests. | ||
func JobLogsResourceHandler(getClient GetClientFn) func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { | ||
return func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { | ||
// Parse parameters from the URI template matcher | ||
owner, ok := request.Params.Arguments["owner"].([]string) | ||
if !ok || len(owner) == 0 { | ||
return nil, errors.New("owner is required") | ||
} | ||
|
||
repo, ok := request.Params.Arguments["repo"].([]string) | ||
if !ok || len(repo) == 0 { | ||
return nil, errors.New("repo is required") | ||
} | ||
|
||
jobIDStr, ok := request.Params.Arguments["jobId"].([]string) | ||
if !ok || len(jobIDStr) == 0 { | ||
return nil, errors.New("jobId is required") | ||
} | ||
|
||
jobID, err := strconv.ParseInt(jobIDStr[0], 10, 64) | ||
if err != nil { | ||
return nil, fmt.Errorf("invalid jobId: %w", err) | ||
} | ||
|
||
client, err := getClient(ctx) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to get GitHub client: %w", err) | ||
} | ||
|
||
// Get the JIT URL for job logs | ||
url, resp, err := client.Actions.GetWorkflowJobLogs(ctx, owner[0], repo[0], jobID, 1) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to get job logs URL: %w", err) | ||
} | ||
defer func() { _ = resp.Body.Close() }() | ||
|
||
// Download the logs content immediately using the JIT URL | ||
content, err := downloadLogsFromJITURL(ctx, url.String()) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to download job logs: %w", err) | ||
} | ||
|
||
return []mcp.ResourceContents{ | ||
mcp.TextResourceContents{ | ||
URI: request.Params.URI, | ||
MIMEType: "text/plain", | ||
Text: content, | ||
}, | ||
}, nil | ||
} | ||
} | ||
|
||
// downloadLogsFromJITURL downloads content from a GitHub JIT URL | ||
func downloadLogsFromJITURL(ctx context.Context, jitURL string) (string, error) { | ||
prof := profiler.New(nil, profiler.IsProfilingEnabled()) | ||
finish := prof.Start(ctx, "download_jit_logs") | ||
|
||
httpResp, err := http.Get(jitURL) //nolint:gosec | ||
if err != nil { | ||
_ = finish(0, 0) | ||
return "", fmt.Errorf("failed to download from JIT URL: %w", err) | ||
} | ||
defer httpResp.Body.Close() | ||
|
||
if httpResp.StatusCode != http.StatusOK { | ||
_ = finish(0, 0) | ||
return "", fmt.Errorf("failed to download logs: HTTP %d", httpResp.StatusCode) | ||
} | ||
|
||
// For large files, we should limit the content size to avoid memory issues | ||
const maxContentSize = 10 * 1024 * 1024 // 10MB limit | ||
|
||
// Read the content with a size limit | ||
content := make([]byte, 0, 1024*1024) // Start with 1MB capacity | ||
buffer := make([]byte, 32*1024) // 32KB read buffer | ||
totalRead := 0 | ||
|
||
for { | ||
n, err := httpResp.Body.Read(buffer) | ||
if n > 0 { | ||
if totalRead+n > maxContentSize { | ||
// Truncate if content is too large | ||
remaining := maxContentSize - totalRead | ||
content = append(content, buffer[:remaining]...) | ||
content = append(content, []byte(fmt.Sprintf("\n\n[Content truncated - original size exceeded %d bytes]", maxContentSize))...) | ||
break | ||
} | ||
content = append(content, buffer[:n]...) | ||
totalRead += n | ||
} | ||
if err != nil { | ||
if err.Error() == "EOF" { | ||
break | ||
} | ||
_ = finish(0, int64(totalRead)) | ||
return "", fmt.Errorf("failed to read response body: %w", err) | ||
} | ||
} | ||
|
||
// Count lines for profiler | ||
lines := 1 | ||
for _, b := range content { | ||
if b == '\n' { | ||
lines++ | ||
} | ||
} | ||
|
||
_ = finish(lines, int64(len(content))) | ||
return string(content), nil | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
package github | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/github/github-mcp-server/pkg/translations" | ||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestGetJobLogsWithResourceLinks(t *testing.T) { | ||
// Test that the tool has the new parameter | ||
tool, _ := GetJobLogs(stubGetClientFn(nil), translations.NullTranslationHelper, 1000) | ||
|
||
// Verify tool has the new parameter | ||
schema := tool.InputSchema | ||
assert.Contains(t, schema.Properties, "return_resource_links") | ||
|
||
// Check that the parameter exists (we can't easily check types in this interface) | ||
resourceLinkParam := schema.Properties["return_resource_links"] | ||
assert.NotNil(t, resourceLinkParam) | ||
} | ||
|
||
func TestJobLogsResourceCreation(t *testing.T) { | ||
// Test that we can create the resource templates without errors | ||
jobLogsResource, jobLogsHandler := GetJobLogsResource(stubGetClientFn(nil), translations.NullTranslationHelper) | ||
workflowRunLogsResource, workflowRunLogsHandler := GetWorkflowRunLogsResource(stubGetClientFn(nil), translations.NullTranslationHelper) | ||
|
||
// Verify resource templates are created | ||
assert.NotNil(t, jobLogsResource) | ||
assert.NotNil(t, jobLogsHandler) | ||
assert.Equal(t, "actions://{owner}/{repo}/jobs/{jobId}/logs", jobLogsResource.URITemplate.Raw()) | ||
|
||
assert.NotNil(t, workflowRunLogsResource) | ||
assert.NotNil(t, workflowRunLogsHandler) | ||
assert.Equal(t, "actions://{owner}/{repo}/runs/{runId}/logs", workflowRunLogsResource.URITemplate.Raw()) | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.