Skip to content

add basic linter workflow #41

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

Merged
merged 1 commit into from
Mar 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions .github/workflows/lint.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: Lint
on:
push:
paths:
- "**.go"
- go.mod
- go.sum
pull_request:
paths:
- "**.go"
- go.mod
- go.sum

permissions:
contents: read

jobs:
lint:
runs-on: ubuntu-latest

steps:
- name: Check out code
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: 'go.mod'

- name: Verify dependencies
run: |
go mod verify
go mod download

LINT_VERSION=1.64.8
curl -fsSL https://github.com/golangci/golangci-lint/releases/download/v${LINT_VERSION}/golangci-lint-${LINT_VERSION}-linux-amd64.tar.gz | \
tar xz --strip-components 1 --wildcards \*/golangci-lint
mkdir -p bin && mv golangci-lint bin/

- name: Run checks
run: |
STATUS=0
assert-nothing-changed() {
local diff
"$@" >/dev/null || return 1
if ! diff="$(git diff -U1 --color --exit-code)"; then
printf '\e[31mError: running `\e[1m%s\e[22m` results in modifications that you must check into version control:\e[0m\n%s\n\n' "$*" "$diff" >&2
git checkout -- .
STATUS=1
fi
}

assert-nothing-changed go fmt ./...
assert-nothing-changed go mod tidy

bin/golangci-lint run --out-format=colored-line-number --timeout=3m || STATUS=$?

exit $STATUS
28 changes: 28 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
run:
timeout: 5m
tests: true
concurrency: 4

linters:
enable:
- govet
- errcheck
- staticcheck
- gofmt
- goimports
- revive
- ineffassign
- typecheck
- unused
- gosimple
- misspell
- nakedret
- bodyclose
- gocritic
- makezero
- gosec

output:
formats: colored-line-number
print-issued-lines: true
print-linter-name: true
12 changes: 6 additions & 6 deletions cmd/github-mcp-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ var (
Use: "stdio",
Short: "Start stdio server",
Long: `Start a server that communicates via standard input/output streams using JSON-RPC messages.`,
Run: func(cmd *cobra.Command, args []string) {
Run: func(_ *cobra.Command, _ []string) {
logFile := viper.GetString("log-file")
readOnly := viper.GetBool("read-only")
exportTranslations := viper.GetBool("export-translations")
Expand All @@ -57,11 +57,11 @@ func init() {
rootCmd.PersistentFlags().String("gh-host", "", "Specify the GitHub hostname (for GitHub Enterprise etc.)")

// Bind flag to viper
viper.BindPFlag("read-only", rootCmd.PersistentFlags().Lookup("read-only"))
viper.BindPFlag("log-file", rootCmd.PersistentFlags().Lookup("log-file"))
viper.BindPFlag("enable-command-logging", rootCmd.PersistentFlags().Lookup("enable-command-logging"))
viper.BindPFlag("export-translations", rootCmd.PersistentFlags().Lookup("export-translations"))
viper.BindPFlag("gh-host", rootCmd.PersistentFlags().Lookup("gh-host"))
_ = viper.BindPFlag("read-only", rootCmd.PersistentFlags().Lookup("read-only"))
_ = viper.BindPFlag("log-file", rootCmd.PersistentFlags().Lookup("log-file"))
_ = viper.BindPFlag("enable-command-logging", rootCmd.PersistentFlags().Lookup("enable-command-logging"))
_ = viper.BindPFlag("export-translations", rootCmd.PersistentFlags().Lookup("export-translations"))
_ = viper.BindPFlag("gh-host", rootCmd.PersistentFlags().Lookup("gh-host"))

// Add subcommands
rootCmd.AddCommand(stdioCmd)
Expand Down
14 changes: 7 additions & 7 deletions cmd/mcpcurl/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"bytes"
"encoding/json"
"fmt"
"golang.org/x/exp/rand"
"io"
"os"
"os/exec"
Expand All @@ -13,6 +12,7 @@ import (

"github.com/spf13/cobra"
"github.com/spf13/viper"
"golang.org/x/exp/rand"
)

type (
Expand Down Expand Up @@ -99,7 +99,7 @@ var (
Use: "mcpcurl",
Short: "CLI tool with dynamically generated commands",
Long: "A CLI tool for interacting with MCP API based on dynamically loaded schemas",
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
PersistentPreRunE: func(cmd *cobra.Command, _ []string) error {
// Skip validation for help and completion commands
if cmd.Name() == "help" || cmd.Name() == "completion" {
return nil
Expand All @@ -119,7 +119,7 @@ var (
Use: "schema",
Short: "Fetch schema from MCP server",
Long: "Fetches the tools schema from the MCP server specified by --stdio-server-cmd",
RunE: func(cmd *cobra.Command, args []string) error {
RunE: func(cmd *cobra.Command, _ []string) error {
serverCmd, _ := cmd.Flags().GetString("stdio-server-cmd")
if serverCmd == "" {
return fmt.Errorf("--stdio-server-cmd is required")
Expand Down Expand Up @@ -206,7 +206,7 @@ func addCommandFromTool(toolsCmd *cobra.Command, tool *Tool, prettyPrint bool) {
cmd := &cobra.Command{
Use: tool.Name,
Short: tool.Description,
Run: func(cmd *cobra.Command, args []string) {
Run: func(cmd *cobra.Command, _ []string) {
// Build a map of arguments from flags
arguments, err := buildArgumentsMap(cmd, tool)
if err != nil {
Expand Down Expand Up @@ -257,15 +257,15 @@ func addCommandFromTool(toolsCmd *cobra.Command, tool *Tool, prettyPrint bool) {
// Enhance description to indicate if parameter is optional
description := prop.Description
if !isRequired {
description = description + " (optional)"
description += " (optional)"
}

switch prop.Type {
case "string":
cmd.Flags().String(name, "", description)
if len(prop.Enum) > 0 {
// Add validation in PreRun for enum values
cmd.PreRunE = func(cmd *cobra.Command, args []string) error {
cmd.PreRunE = func(cmd *cobra.Command, _ []string) error {
for flagName, property := range tool.InputSchema.Properties {
if len(property.Enum) > 0 {
value, _ := cmd.Flags().GetString(flagName)
Expand Down Expand Up @@ -373,7 +373,7 @@ func executeServerCommand(cmdStr, jsonRequest string) (string, error) {
return "", fmt.Errorf("empty command")
}

cmd := exec.Command(cmdParts[0], cmdParts[1:]...)
cmd := exec.Command(cmdParts[0], cmdParts[1:]...) //nolint:gosec //mcpcurl is a test command that needs to execute arbitrary shell commands

// Setup stdin pipe
stdin, err := cmd.StdinPipe()
Expand Down
4 changes: 2 additions & 2 deletions pkg/github/code_scanning_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func Test_GetCodeScanningAlert(t *testing.T) {
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetReposCodeScanningAlertsByOwnerByRepoByAlertNumber,
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNotFound)
_, _ = w.Write([]byte(`{"message": "Not Found"}`))
}),
Expand Down Expand Up @@ -176,7 +176,7 @@ func Test_ListCodeScanningAlerts(t *testing.T) {
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetReposCodeScanningAlertsByOwnerByRepo,
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusUnauthorized)
_, _ = w.Write([]byte(`{"message": "Unauthorized access"}`))
}),
Expand Down
6 changes: 3 additions & 3 deletions pkg/github/helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,19 @@ package github

import (
"encoding/json"
"github.com/stretchr/testify/assert"
"net/http"
"testing"

"github.com/mark3labs/mcp-go/mcp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// mockResponse is a helper function to create a mock HTTP response handler
// that returns a specified status code and marshalled body.
// that returns a specified status code and marshaled body.
func mockResponse(t *testing.T, code int, body interface{}) http.HandlerFunc {
t.Helper()
return func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(code)
b, err := json.Marshal(body)
require.NoError(t, err)
Expand Down
16 changes: 8 additions & 8 deletions pkg/github/issues_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ func Test_AddIssueComment(t *testing.T) {
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.PostReposIssuesCommentsByOwnerByRepoByIssueNumber,
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusUnprocessableEntity)
_, _ = w.Write([]byte(`{"message": "Invalid request"}`))
}),
Expand Down Expand Up @@ -323,7 +323,7 @@ func Test_SearchIssues(t *testing.T) {
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetSearchIssues,
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte(`{"message": "Validation Failed"}`))
}),
Expand Down Expand Up @@ -463,7 +463,7 @@ func Test_CreateIssue(t *testing.T) {
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.PostReposIssuesByOwnerByRepo,
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusUnprocessableEntity)
_, _ = w.Write([]byte(`{"message": "Validation failed"}`))
}),
Expand Down Expand Up @@ -646,7 +646,7 @@ func Test_ListIssues(t *testing.T) {
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetReposIssuesByOwnerByRepo,
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNotFound)
_, _ = w.Write([]byte(`{"message": "Repository not found"}`))
}),
Expand Down Expand Up @@ -799,7 +799,7 @@ func Test_UpdateIssue(t *testing.T) {
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.PatchReposIssuesByOwnerByRepoByIssueNumber,
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNotFound)
_, _ = w.Write([]byte(`{"message": "Issue not found"}`))
}),
Expand All @@ -819,7 +819,7 @@ func Test_UpdateIssue(t *testing.T) {
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.PatchReposIssuesByOwnerByRepoByIssueNumber,
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusUnprocessableEntity)
_, _ = w.Write([]byte(`{"message": "Invalid state value"}`))
}),
Expand Down Expand Up @@ -881,15 +881,15 @@ func Test_UpdateIssue(t *testing.T) {
}

// Check assignees if expected
if tc.expectedIssue.Assignees != nil && len(tc.expectedIssue.Assignees) > 0 {
if len(tc.expectedIssue.Assignees) > 0 {
assert.Len(t, returnedIssue.Assignees, len(tc.expectedIssue.Assignees))
for i, assignee := range returnedIssue.Assignees {
assert.Equal(t, *tc.expectedIssue.Assignees[i].Login, *assignee.Login)
}
}

// Check labels if expected
if tc.expectedIssue.Labels != nil && len(tc.expectedIssue.Labels) > 0 {
if len(tc.expectedIssue.Labels) > 0 {
assert.Len(t, returnedIssue.Labels, len(tc.expectedIssue.Labels))
for i, label := range returnedIssue.Labels {
assert.Equal(t, *tc.expectedIssue.Labels[i].Name, *label.Name)
Expand Down
22 changes: 11 additions & 11 deletions pkg/github/pullrequests_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func Test_GetPullRequest(t *testing.T) {
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetReposPullsByOwnerByRepoByPullNumber,
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNotFound)
_, _ = w.Write([]byte(`{"message": "Not Found"}`))
}),
Expand Down Expand Up @@ -193,7 +193,7 @@ func Test_ListPullRequests(t *testing.T) {
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetReposPullsByOwnerByRepo,
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte(`{"message": "Invalid request"}`))
}),
Expand Down Expand Up @@ -302,7 +302,7 @@ func Test_MergePullRequest(t *testing.T) {
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.PutReposPullsMergeByOwnerByRepoByPullNumber,
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusMethodNotAllowed)
_, _ = w.Write([]byte(`{"message": "Pull request cannot be merged"}`))
}),
Expand Down Expand Up @@ -414,7 +414,7 @@ func Test_GetPullRequestFiles(t *testing.T) {
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetReposPullsFilesByOwnerByRepoByPullNumber,
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNotFound)
_, _ = w.Write([]byte(`{"message": "Not Found"}`))
}),
Expand Down Expand Up @@ -551,7 +551,7 @@ func Test_GetPullRequestStatus(t *testing.T) {
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetReposPullsByOwnerByRepoByPullNumber,
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNotFound)
_, _ = w.Write([]byte(`{"message": "Not Found"}`))
}),
Expand All @@ -574,7 +574,7 @@ func Test_GetPullRequestStatus(t *testing.T) {
),
mock.WithRequestMatchHandler(
mock.GetReposCommitsStatusesByOwnerByRepoByRef,
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNotFound)
_, _ = w.Write([]byte(`{"message": "Not Found"}`))
}),
Expand Down Expand Up @@ -695,7 +695,7 @@ func Test_UpdatePullRequestBranch(t *testing.T) {
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.PutReposPullsUpdateBranchByOwnerByRepoByPullNumber,
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusConflict)
_, _ = w.Write([]byte(`{"message": "Merge conflict"}`))
}),
Expand Down Expand Up @@ -811,7 +811,7 @@ func Test_GetPullRequestComments(t *testing.T) {
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetReposPullsCommentsByOwnerByRepoByPullNumber,
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNotFound)
_, _ = w.Write([]byte(`{"message": "Not Found"}`))
}),
Expand Down Expand Up @@ -934,7 +934,7 @@ func Test_GetPullRequestReviews(t *testing.T) {
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetReposPullsReviewsByOwnerByRepoByPullNumber,
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNotFound)
_, _ = w.Write([]byte(`{"message": "Not Found"}`))
}),
Expand Down Expand Up @@ -1099,7 +1099,7 @@ func Test_CreatePullRequestReview(t *testing.T) {
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.PostReposPullsReviewsByOwnerByRepoByPullNumber,
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusUnprocessableEntity)
_, _ = w.Write([]byte(`{"message": "Invalid comment format"}`))
}),
Expand All @@ -1126,7 +1126,7 @@ func Test_CreatePullRequestReview(t *testing.T) {
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.PostReposPullsReviewsByOwnerByRepoByPullNumber,
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusUnprocessableEntity)
_, _ = w.Write([]byte(`{"message": "Invalid comment format"}`))
}),
Expand Down
Loading