196 lines
6.4 KiB
Go
196 lines
6.4 KiB
Go
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
|
|
}
|