Skip to content

Conversation

@philprime
Copy link
Member

Summary

This PR adds a new get_readme MCP tool and refactors the codebase to reduce duplication and improve maintainability.

New Features

1. get_readme Tool

  • Fetches README.md files from GitHub repositories
  • Supports multiple formats: owner/repo@branch, owner/repo@tag, or owner/repo (defaults to main)
  • Tries multiple README filename variations (README.md, readme.md, Readme.md, README, readme)
  • Returns full markdown content for AI consumption

2. Enhanced action.yml Support

  • Now supports both action.yml and action.yaml filename conventions
  • Automatically tries both filenames for better compatibility

Code Improvements

Refactoring

  • Created internal/github/ref.go with shared reference parsing logic
  • Unified ActionRef and RepoRef into a single Ref struct
  • Both ParseActionRef and ParseRepoRef now use the shared ParseRef function
  • Created shared FetchRawFile method for fetching files from GitHub
  • Reduced code duplication significantly

Testing

  • Added comprehensive tests for ParseRef function
  • Updated existing tests to use unified structure
  • All tests passing ✅

Breaking Changes

None - all existing functionality is maintained and backward compatible.

Files Changed

  • internal/github/ref.go - New shared reference parsing module
  • internal/github/ref_test.go - Tests for reference parsing
  • internal/github/actions.go - Refactored to use shared code
  • internal/github/actions_test.go - Updated tests
  • internal/cli/mcp/tools.go - Added get_readme handler
  • internal/cli/mcp/server.go - Registered new tool

Testing

make test  # All tests pass
make build # Builds successfully

Verified both tools work correctly:

  • get_action_parameters - Fetches action.yml/action.yaml
  • get_readme - Fetches README from repositories

- Add new get_readme MCP tool to fetch README files from GitHub repositories
- Support both README.md and readme.md filename variations
- Add support for action.yaml extension (in addition to action.yml)
- Refactor reference parsing into shared ref.go module
- Unify ActionRef and RepoRef into single Ref struct with flexible parsing
- Add comprehensive tests for reference parsing
- Reuse FetchRawFile helper for both actions and README fetching

Breaking changes: None - all existing functionality maintained

Closes #N/A
@philprime philprime enabled auto-merge (squash) November 10, 2025 13:57
@philprime philprime requested a review from Copilot November 10, 2025 13:57
@philprime philprime disabled auto-merge November 10, 2025 13:57
@philprime philprime merged commit 37a77b9 into main Nov 10, 2025
8 checks passed
@philprime philprime deleted the feature/add-get-readme-tool branch November 10, 2025 13:58
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds support for fetching README files from GitHub repositories by introducing a new get_readme MCP tool and refactoring the existing action reference parsing logic. The changes extract common GitHub reference parsing into a reusable ParseRef function and add new functionality to fetch README files from repositories.

Key Changes:

  • Refactored action reference parsing into a generic ParseRef function that supports both actions and repositories
  • Added ParseRepoRef, FetchReadme, GetReadme, and FetchRawFile functions to support README retrieval
  • Introduced a new get_readme MCP tool for fetching repository README files

Reviewed Changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
internal/github/ref.go Introduces the generic Ref type and ParseRef function for parsing GitHub references, plus FetchRawFile helper for fetching raw content from GitHub CDN
internal/github/ref_test.go Comprehensive tests for the new ParseRef function covering various input scenarios and edge cases
internal/github/actions.go Refactors ParseActionRef to use ParseRef, adds ParseRepoRef, FetchReadme, and GetReadme functions, and updates FetchActionYAML to support both action.yml and action.yaml
internal/github/actions_test.go Adds TestParseRepoRef with comprehensive test cases for the new repository reference parsing function
internal/cli/mcp/tools.go Implements handleGetReadme handler for the new get_readme MCP tool with input validation and error handling
internal/cli/mcp/server.go Registers the new get_readme tool with appropriate description and Sentry tracing

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

func (s *ActionsService) FetchReadme(owner, repo, ref string) (string, error) {
// Try common README filenames in order of preference
readmeNames := []string{"README.md", "readme.md", "Readme.md", "README", "readme"}
urlPath := fmt.Sprintf("refs/heads/%s", ref)
Copy link

Copilot AI Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The FetchReadme function always constructs the URL path as refs/heads/{ref}, which assumes the ref is a branch name. However, according to the function's documentation and the ParseRepoRef examples, the ref can also be a tag (e.g., "v5") or a commit SHA (e.g., "abc123def456").

When the ref is a tag or commit SHA, the URL should use refs/tags/{tag} or just {sha} respectively. Using refs/heads/ for these cases will result in 404 errors.

Consider updating the logic to handle different ref types appropriately, similar to how FetchActionYAML uses refs/tags/{version} for tags, or allow the caller to specify the URL path type.

Copilot uses AI. Check for mistakes.
}

return data, nil
return nil, fmt.Errorf("action.yml or action.yaml not found for %s/%s@%s", owner, repo, version)
Copy link

Copilot AI Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function can return nil, nil when lastErr is nil, which is unreachable in practice since the loop always assigns to lastErr on error. However, if the actionFilenames slice were empty, this would be possible.

Consider simplifying the error handling by removing line 54 and only keeping the check on line 51-52, or adding a defensive check for an empty actionFilenames slice.

Suggested change
return nil, fmt.Errorf("action.yml or action.yaml not found for %s/%s@%s", owner, repo, version)

Copilot uses AI. Check for mistakes.
Comment on lines +138 to +142
// If we get here, none of the README files were found
if lastErr != nil {
return "", fmt.Errorf("README not found in repository %s/%s@%s: %w", owner, repo, ref, lastErr)
}
return "", fmt.Errorf("README not found in repository %s/%s@%s", owner, repo, ref)
Copy link

Copilot AI Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the issue in FetchActionYAML, this code can return nil, "" when lastErr is nil, which is unreachable in practice since the loop always assigns to lastErr on error. However, if the readmeNames slice were empty, this would be possible.

Consider simplifying the error handling by removing line 142 and only keeping the check on line 139-140, or adding a defensive check for an empty readmeNames slice.

Copilot uses AI. Check for mistakes.
Comment on lines +76 to +108
func (s *ActionsService) FetchRawFile(owner, repo, urlPath, filename string) ([]byte, error) {
// Construct URL to raw file on GitHub
url := fmt.Sprintf("https://raw.githubusercontent.com/%s/%s/%s/%s",
owner, repo, urlPath, filename)

// Make HTTP GET request
resp, err := s.httpClient.Get(url)
if err != nil {
return nil, fmt.Errorf("failed to fetch %s: %w", filename, err)
}
defer resp.Body.Close()

// Check for HTTP errors
if resp.StatusCode != 200 {
if resp.StatusCode == 404 {
return nil, fmt.Errorf("%s not found at %s (status: 404)", filename, url)
}
return nil, fmt.Errorf("failed to fetch %s from %s (status: %d)", filename, url, resp.StatusCode)
}

// Read response body
data, err := readAllBody(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read %s response: %w", filename, err)
}

return data, nil
}

// readAllBody is a helper to read all data from an io.Reader.
func readAllBody(r io.Reader) ([]byte, error) {
return io.ReadAll(r)
}
Copy link

Copilot AI Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The FetchRawFile method is defined in ref.go but is a method on ActionsService, which is defined in actions.go. This creates a split definition where the type and its methods are defined across multiple files.

For better code organization and maintainability, consider either:

  1. Moving FetchRawFile to actions.go where ActionsService is defined, or
  2. Moving ActionsService to a more general file if it's meant to handle both actions and general repository operations

This would make it easier to understand the complete API surface of ActionsService and follow Go best practices of keeping related code together.

Suggested change
func (s *ActionsService) FetchRawFile(owner, repo, urlPath, filename string) ([]byte, error) {
// Construct URL to raw file on GitHub
url := fmt.Sprintf("https://raw.githubusercontent.com/%s/%s/%s/%s",
owner, repo, urlPath, filename)
// Make HTTP GET request
resp, err := s.httpClient.Get(url)
if err != nil {
return nil, fmt.Errorf("failed to fetch %s: %w", filename, err)
}
defer resp.Body.Close()
// Check for HTTP errors
if resp.StatusCode != 200 {
if resp.StatusCode == 404 {
return nil, fmt.Errorf("%s not found at %s (status: 404)", filename, url)
}
return nil, fmt.Errorf("failed to fetch %s from %s (status: %d)", filename, url, resp.StatusCode)
}
// Read response body
data, err := readAllBody(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read %s response: %w", filename, err)
}
return data, nil
}
// readAllBody is a helper to read all data from an io.Reader.
func readAllBody(r io.Reader) ([]byte, error) {
return io.ReadAll(r)
}
// Moved to actions.go with ActionsService definition.

Copilot uses AI. Check for mistakes.
Comment on lines +105 to +108
// readAllBody is a helper to read all data from an io.Reader.
func readAllBody(r io.Reader) ([]byte, error) {
return io.ReadAll(r)
}
Copy link

Copilot AI Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The readAllBody helper function is a thin wrapper around io.ReadAll that provides no additional functionality. Since io.ReadAll is already clear and concise, this wrapper adds an unnecessary layer of indirection without adding value.

Consider using io.ReadAll directly at line 97 instead of maintaining this single-use helper function.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants