From a79a379f8740440deb53798e6668cc82a7f30b4c Mon Sep 17 00:00:00 2001 From: Muhammad Azeez Date: Tue, 28 Jan 2025 14:45:27 +0300 Subject: [PATCH 1/2] feat: add list issues/prs to github servlet --- servlets/github/branches.go | 149 ++++++++++++++++++++++++++++++++++++ servlets/github/issues.go | 107 ++++++++++++++++++++++++++ servlets/github/main.go | 9 +++ 3 files changed, 265 insertions(+) diff --git a/servlets/github/branches.go b/servlets/github/branches.go index fb805c6..acd3557 100644 --- a/servlets/github/branches.go +++ b/servlets/github/branches.go @@ -3,6 +3,7 @@ package main import ( "encoding/json" "fmt" + "strings" "github.com/extism/go-pdk" ) @@ -22,6 +23,26 @@ var ( "required": []string{"owner", "repo", "branch", "from_branch"}, }, } + ListPullRequestsTool = ToolDescription{ + Name: "gh-list-pull-requests", + Description: "Lists pull requests in a specified repository. Supports different response formats via accept parameter.", + InputSchema: schema{ + "type": "object", + "properties": props{ + "owner": prop("string", "The account owner of the repository. The name is not case sensitive."), + "repo": prop("string", "The name of the repository without the .git extension. The name is not case sensitive."), + "state": prop("string", "Either open, closed, or all to filter by state."), + "head": prop("string", "Filter pulls by head user or head organization and branch name in the format of user:ref-name or organization:ref-name."), + "base": prop("string", "Filter pulls by base branch name. Example: gh-pages"), + "sort": prop("string", "What to sort results by. Can be one of: created, updated, popularity, long-running"), + "direction": prop("string", "The direction of the sort. Default: desc when sort is created or not specified, otherwise asc"), + "per_page": prop("integer", "The number of results per page (max 100)"), + "page": prop("integer", "The page number of the results to fetch"), + "accept": prop("string", "Response format: raw (default), text, html, or full. Raw returns body, text returns body_text, html returns body_html, full returns all."), + }, + "required": []string{"owner", "repo"}, + }, + } CreatePullRequestTool = ToolDescription{ Name: "gh-create-pull-request", Description: "Create a pull request in a GitHub repository", @@ -44,6 +65,7 @@ var ( var BranchTools = []ToolDescription{ CreateBranchTool, + ListPullRequestsTool, CreatePullRequestTool, } @@ -142,6 +164,133 @@ func branchPullRequestSchemaFromArgs(args map[string]interface{}) PullRequestSch return prs } +func pullRequestList(apiKey string, owner, repo string, args map[string]interface{}) (CallToolResult, error) { + baseURL := fmt.Sprintf("https://api.github.com/repos/%s/%s/pulls", owner, repo) + params := make([]string, 0) + + // Handle state parameter + if state, ok := args["state"].(string); ok && state != "" { + switch state { + case "open", "closed", "all": + params = append(params, fmt.Sprintf("state=%s", state)) + } + } else { + params = append(params, "state=open") // Default value + } + + // Handle head parameter (user:ref-name or organization:ref-name format) + if head, ok := args["head"].(string); ok && head != "" { + params = append(params, fmt.Sprintf("head=%s", head)) + } + + // Handle base parameter + if base, ok := args["base"].(string); ok && base != "" { + params = append(params, fmt.Sprintf("base=%s", base)) + } + + // Handle sort parameter + sort := "created" // Default value + if sortArg, ok := args["sort"].(string); ok && sortArg != "" { + switch sortArg { + case "created", "updated", "popularity", "long-running": + sort = sortArg + } + } + params = append(params, fmt.Sprintf("sort=%s", sort)) + + // Handle direction parameter + direction := "desc" // Default for created or unspecified sort + if sort != "created" { + direction = "asc" // Default for other sort types + } + if dirArg, ok := args["direction"].(string); ok { + switch dirArg { + case "asc", "desc": + direction = dirArg + } + } + params = append(params, fmt.Sprintf("direction=%s", direction)) + + // Handle pagination + perPage := 30 // Default value + if perPageArg, ok := args["per_page"].(float64); ok { + if perPageArg > 100 { + perPage = 100 // Max value + } else if perPageArg > 0 { + perPage = int(perPageArg) + } + } + params = append(params, fmt.Sprintf("per_page=%d", perPage)) + + page := 1 // Default value + if pageArg, ok := args["page"].(float64); ok && pageArg > 0 { + page = int(pageArg) + } + params = append(params, fmt.Sprintf("page=%d", page)) + + // Build final URL + url := fmt.Sprintf("%s?%s", baseURL, strings.Join(params, "&")) + pdk.Log(pdk.LogDebug, fmt.Sprint("Listing pull requests: ", url)) + + // Make request + req := pdk.NewHTTPRequest(pdk.MethodGet, url) + req.SetHeader("Authorization", fmt.Sprint("token ", apiKey)) + + // Handle Accept header based on requested format + acceptHeader := "application/vnd.github+json" // Default recommended header + if format, ok := args["accept"].(string); ok { + switch format { + case "raw": + acceptHeader = "application/vnd.github.raw+json" + case "text": + acceptHeader = "application/vnd.github.text+json" + case "html": + acceptHeader = "application/vnd.github.html+json" + case "full": + acceptHeader = "application/vnd.github.full+json" + } + } + req.SetHeader("Accept", acceptHeader) + req.SetHeader("User-Agent", "github-mcpx-servlet") + + resp := req.Send() + + // Handle response status codes + switch resp.Status() { + case 200: + return CallToolResult{ + Content: []Content{{ + Type: ContentTypeText, + Text: some(string(resp.Body())), + }}, + }, nil + case 304: + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some("Not modified"), + }}, + }, nil + case 422: + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some("Validation failed, or the endpoint has been spammed."), + }}, + }, nil + default: + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some(fmt.Sprintf("Request failed with status %d: %s", resp.Status(), string(resp.Body()))), + }}, + }, nil + } +} + func branchCreatePullRequest(apiKey, owner, repo string, pr PullRequestSchema) CallToolResult { url := fmt.Sprintf("https://api.github.com/repos/%s/%s/pulls", owner, repo) req := pdk.NewHTTPRequest(pdk.MethodPost, url) diff --git a/servlets/github/issues.go b/servlets/github/issues.go index 92091e2..87a6068 100644 --- a/servlets/github/issues.go +++ b/servlets/github/issues.go @@ -3,11 +3,36 @@ package main import ( "encoding/json" "fmt" + "strings" "github.com/extism/go-pdk" ) var ( + ListIssuesTool = ToolDescription{ + Name: "gh-list-issues", + Description: "List issues from a GitHub repository", + InputSchema: schema{ + "type": "object", + "properties": props{ + "owner": prop("string", "The owner of the repository"), + "repo": prop("string", "The repository name"), + "filter": prop("string", "Filter by assigned, created, mentioned, subscribed, repos, all"), + "state": prop("string", "The state of the issues (open, closed, all)"), + "labels": prop("string", "A list of comma separated label names (e.g. bug,ui,@high)"), + "sort": prop("string", "Sort field (created, updated, comments)"), + "direction": prop("string", "Sort direction (asc or desc)"), + "since": prop("string", "ISO 8601 timestamp (YYYY-MM-DDTHH:MM:SSZ)"), + "collab": prop("boolean", "Filter by issues that are collaborated on"), + "orgs": prop("boolean", "Filter by organization issues"), + "owned": prop("boolean", "Filter by owned issues"), + "pulls": prop("boolean", "Include pull requests in results"), + "per_page": prop("integer", "Number of results per page (max 100)"), + "page": prop("integer", "Page number for pagination"), + }, + "required": []string{"owner", "repo"}, + }, + } CreateIssueTool = ToolDescription{ Name: "gh-create-issue", Description: "Create an issue on a GitHub repository", @@ -71,6 +96,7 @@ var ( }, } IssueTools = []ToolDescription{ + ListIssuesTool, CreateIssueTool, GetIssueTool, UpdateIssueTool, @@ -86,6 +112,87 @@ type Issue struct { Labels []string `json:"labels,omitempty"` } +func issueList(apiKey string, owner, repo string, args map[string]interface{}) (CallToolResult, error) { + baseURL := fmt.Sprintf("https://api.github.com/repos/%s/%s/issues", owner, repo) + params := make([]string, 0) + + // String parameters + stringParams := map[string]string{ + "filter": "assigned", // Default value + "state": "open", // Default value + "labels": "", + "sort": "created", // Default value + "direction": "desc", // Default value + "since": "", + } + + for key := range stringParams { + if value, ok := args[key].(string); ok && value != "" { + params = append(params, fmt.Sprintf("%s=%s", key, value)) + } else if stringParams[key] != "" { + // Add default value if one exists + params = append(params, fmt.Sprintf("%s=%s", key, stringParams[key])) + } + } + + // Boolean parameters + boolParams := []string{"collab", "orgs", "owned", "pulls"} + for _, param := range boolParams { + if value, ok := args[param].(bool); ok { + params = append(params, fmt.Sprintf("%s=%t", param, value)) + } + } + + // Pagination parameters + perPage := 30 // Default value + if value, ok := args["per_page"].(float64); ok { + if value > 100 { + perPage = 100 // Max value + } else if value > 0 { + perPage = int(value) + } + } + params = append(params, fmt.Sprintf("per_page=%d", perPage)) + + page := 1 // Default value + if value, ok := args["page"].(float64); ok && value > 0 { + page = int(value) + } + params = append(params, fmt.Sprintf("page=%d", page)) + + // Build final URL + url := baseURL + if len(params) > 0 { + url = fmt.Sprintf("%s?%s", baseURL, strings.Join(params, "&")) + } + + pdk.Log(pdk.LogDebug, fmt.Sprint("Listing issues: ", url)) + + // Make request + req := pdk.NewHTTPRequest(pdk.MethodGet, url) + req.SetHeader("Authorization", fmt.Sprint("token ", apiKey)) + req.SetHeader("Accept", "application/vnd.github+json") + req.SetHeader("User-Agent", "github-mcpx-servlet") + + resp := req.Send() + if resp.Status() != 200 { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some(fmt.Sprintf("Failed to list issues: %d %s", resp.Status(), string(resp.Body()))), + }}, + }, nil + } + + return CallToolResult{ + Content: []Content{{ + Type: ContentTypeText, + Text: some(string(resp.Body())), + }}, + }, nil +} + func issueFromArgs(args map[string]interface{}) Issue { data := Issue{} if title, ok := args["title"].(string); ok { diff --git a/servlets/github/main.go b/servlets/github/main.go index 80616bf..821d799 100755 --- a/servlets/github/main.go +++ b/servlets/github/main.go @@ -27,6 +27,10 @@ func Call(input CallToolRequest) (CallToolResult, error) { args := input.Params.Arguments.(map[string]interface{}) pdk.Log(pdk.LogDebug, fmt.Sprint("Args: ", args)) switch input.Params.Name { + case ListIssuesTool.Name: + owner, _ := args["owner"].(string) + repo, _ := args["repo"].(string) + return issueList(apiKey, owner, repo, args) case GetIssueTool.Name: owner, _ := args["owner"].(string) repo, _ := args["repo"].(string) @@ -74,6 +78,11 @@ func Call(input CallToolRequest) (CallToolResult, error) { } return branchCreate(apiKey, owner, repo, from, maybeBranch), nil + case ListPullRequestsTool.Name: + owner, _ := args["owner"].(string) + repo, _ := args["repo"].(string) + return pullRequestList(apiKey, owner, repo, args) + case CreatePullRequestTool.Name: owner, _ := args["owner"].(string) repo, _ := args["repo"].(string) From 8859ee348a28022344cfa0094f490c65e9ddc074 Mon Sep 17 00:00:00 2001 From: Muhammad Azeez Date: Tue, 28 Jan 2025 15:18:49 +0300 Subject: [PATCH 2/2] add repo tools --- servlets/github/main.go | 34 +++- servlets/github/repo.go | 412 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 445 insertions(+), 1 deletion(-) create mode 100644 servlets/github/repo.go diff --git a/servlets/github/main.go b/servlets/github/main.go index 821d799..0319ea6 100755 --- a/servlets/github/main.go +++ b/servlets/github/main.go @@ -97,6 +97,25 @@ func Call(input CallToolRequest) (CallToolResult, error) { files := filePushFromArgs(args) return filesPush(apiKey, owner, repo, branch, message, files), nil + case ListReposTool.Name: + owner, _ := args["owner"].(string) + return reposList(apiKey, owner, args) + + case GetRepositoryCollaboratorsTool.Name: + owner, _ := args["owner"].(string) + repo, _ := args["repo"].(string) + return reposGetCollaborators(apiKey, owner, repo, args) + + case GetRepositoryContributorsTool.Name: + owner, _ := args["owner"].(string) + repo, _ := args["repo"].(string) + return reposGetContributors(apiKey, owner, repo, args) + + case GetRepositoryDetailsTool.Name: + owner, _ := args["owner"].(string) + repo, _ := args["repo"].(string) + return reposGetDetails(apiKey, owner, repo) + default: return CallToolResult{ IsError: some(true), @@ -110,8 +129,21 @@ func Call(input CallToolRequest) (CallToolResult, error) { } func Describe() (ListToolsResult, error) { + toolsets := [][]ToolDescription{ + IssueTools, + FileTools, + BranchTools, + RepoTools, + } + + tools := []ToolDescription{} + + for _, toolset := range toolsets { + tools = append(tools, toolset...) + } + return ListToolsResult{ - Tools: append(IssueTools, append(FileTools, BranchTools...)...), + Tools: tools, }, nil } diff --git a/servlets/github/repo.go b/servlets/github/repo.go new file mode 100644 index 0000000..a73d40a --- /dev/null +++ b/servlets/github/repo.go @@ -0,0 +1,412 @@ +package main + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/extism/go-pdk" +) + +var ( + GetRepositoryContributorsTool = ToolDescription{ + Name: "gh-get-repo-contributors", + Description: "Get the list of contributors for a GitHub repository, including their contributions count and profile details", + InputSchema: schema{ + "type": "object", + "properties": props{ + "owner": prop("string", "The owner of the repository"), + "repo": prop("string", "The repository name"), + "per_page": prop("integer", "Number of results per page (max 100)"), + "page": prop("integer", "Page number for pagination"), + }, + "required": []string{"owner", "repo"}, + }, + } + GetRepositoryCollaboratorsTool = ToolDescription{ + Name: "gh-get-repo-collaborators", + Description: "Get the list of collaborators for a GitHub repository, including their permissions and profile details", + InputSchema: schema{ + "type": "object", + "properties": props{ + "owner": prop("string", "The owner of the repository"), + "repo": prop("string", "The repository name"), + "per_page": prop("integer", "Number of results per page (max 100)"), + "page": prop("integer", "Page number for pagination"), + }, + "required": []string{"owner", "repo"}, + }, + } + GetRepositoryDetailsTool = ToolDescription{ + Name: "gh-get-repo-details", + Description: "Get detailed information about a GitHub repository, including stars, forks, issues, and more", + InputSchema: schema{ + "type": "object", + "properties": props{ + "owner": prop("string", "The owner of the repository"), + "repo": prop("string", "The repository name"), + }, + "required": []string{"owner", "repo"}, + }, + } + ListReposTool = ToolDescription{ + Name: "gh-list-repos", + Description: "List repositories for a GitHub user or organization", + InputSchema: schema{ + "type": "object", + "properties": props{ + "username": prop("string", "The GitHub username or organization name"), + "type": prop("string", "The type of repositories to list (all, owner, member)"), + "sort": prop("string", "The sort field (created, updated, pushed, full_name)"), + "direction": prop("string", "The sort direction (asc or desc)"), + "per_page": prop("integer", "Number of results per page (max 100)"), + "page": prop("integer", "Page number for pagination"), + }, + "required": []string{"username"}, + }, + } + RepoTools = []ToolDescription{ + GetRepositoryContributorsTool, + GetRepositoryCollaboratorsTool, + GetRepositoryDetailsTool, + ListReposTool, + } +) + +type Contributor struct { + Login string `json:"login"` + ID int `json:"id"` + NodeID string `json:"node_id"` + AvatarURL string `json:"avatar_url"` + GravatarID string `json:"gravatar_id"` + URL string `json:"url"` + HTMLURL string `json:"html_url"` + FollowersURL string `json:"followers_url"` + FollowingURL string `json:"following_url"` + GistsURL string `json:"gists_url"` + StarredURL string `json:"starred_url"` + SubscriptionsURL string `json:"subscriptions_url"` + OrganizationsURL string `json:"organizations_url"` + ReposURL string `json:"repos_url"` + EventsURL string `json:"events_url"` + ReceivedEventsURL string `json:"received_events_url"` + Type string `json:"type"` + SiteAdmin bool `json:"site_admin"` + Contributions int `json:"contributions"` +} + +func reposGetContributors(apiKey string, owner, repo string, args map[string]interface{}) (CallToolResult, error) { + baseURL := fmt.Sprintf("https://api.github.com/repos/%s/%s/contributors", owner, repo) + params := make([]string, 0) + + // Pagination parameters + perPage := 30 // Default value + if value, ok := args["per_page"].(float64); ok { + if value > 100 { + perPage = 100 // Max value + } else if value > 0 { + perPage = int(value) + } + } + params = append(params, fmt.Sprintf("per_page=%d", perPage)) + + page := 1 // Default value + if value, ok := args["page"].(float64); ok && value > 0 { + page = int(value) + } + params = append(params, fmt.Sprintf("page=%d", page)) + + // Build final URL + url := baseURL + if len(params) > 0 { + url = fmt.Sprintf("%s?%s", baseURL, strings.Join(params, "&")) + } + + pdk.Log(pdk.LogDebug, fmt.Sprint("Fetching contributors: ", url)) + + // Make request + req := pdk.NewHTTPRequest(pdk.MethodGet, url) + req.SetHeader("Authorization", fmt.Sprint("token ", apiKey)) + req.SetHeader("Accept", "application/vnd.github+json") + req.SetHeader("User-Agent", "github-mcpx-servlet") + + resp := req.Send() + if resp.Status() != 200 { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some(fmt.Sprintf("Failed to fetch contributors: %d %s", resp.Status(), string(resp.Body()))), + }}, + }, nil + } + + // Parse the response + var contributors []Contributor + if err := json.Unmarshal(resp.Body(), &contributors); err != nil { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some(fmt.Sprintf("Failed to parse contributors: %s", err)), + }}, + }, nil + } + + // Marshal the response + responseJSON, err := json.Marshal(contributors) + if err != nil { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some(fmt.Sprintf("Failed to marshal response: %s", err)), + }}, + }, nil + } + + return CallToolResult{ + Content: []Content{{ + Type: ContentTypeText, + Text: some(string(responseJSON)), + }}, + }, nil +} + +type Collaborator struct { + Login string `json:"login"` + ID int `json:"id"` + NodeID string `json:"node_id"` + AvatarURL string `json:"avatar_url"` + GravatarID string `json:"gravatar_id"` + URL string `json:"url"` + HTMLURL string `json:"html_url"` + FollowersURL string `json:"followers_url"` + FollowingURL string `json:"following_url"` + GistsURL string `json:"gists_url"` + StarredURL string `json:"starred_url"` + SubscriptionsURL string `json:"subscriptions_url"` + OrganizationsURL string `json:"organizations_url"` + ReposURL string `json:"repos_url"` + EventsURL string `json:"events_url"` + ReceivedEventsURL string `json:"received_events_url"` + Type string `json:"type"` + SiteAdmin bool `json:"site_admin"` + Permissions struct { + Admin bool `json:"admin"` + Push bool `json:"push"` + Pull bool `json:"pull"` + } `json:"permissions"` +} + +func reposGetCollaborators(apiKey string, owner, repo string, args map[string]interface{}) (CallToolResult, error) { + baseURL := fmt.Sprintf("https://api.github.com/repos/%s/%s/collaborators", owner, repo) + params := make([]string, 0) + + // Pagination parameters + perPage := 30 // Default value + if value, ok := args["per_page"].(float64); ok { + if value > 100 { + perPage = 100 // Max value + } else if value > 0 { + perPage = int(value) + } + } + params = append(params, fmt.Sprintf("per_page=%d", perPage)) + + page := 1 // Default value + if value, ok := args["page"].(float64); ok && value > 0 { + page = int(value) + } + params = append(params, fmt.Sprintf("page=%d", page)) + + // Build final URL + url := baseURL + if len(params) > 0 { + url = fmt.Sprintf("%s?%s", baseURL, strings.Join(params, "&")) + } + + pdk.Log(pdk.LogDebug, fmt.Sprint("Fetching collaborators: ", url)) + + // Make request + req := pdk.NewHTTPRequest(pdk.MethodGet, url) + req.SetHeader("Authorization", fmt.Sprint("token ", apiKey)) + req.SetHeader("Accept", "application/vnd.github+json") + req.SetHeader("User-Agent", "github-mcpx-servlet") + + resp := req.Send() + if resp.Status() != 200 { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some(fmt.Sprintf("Failed to fetch collaborators: %d %s", resp.Status(), string(resp.Body()))), + }}, + }, nil + } + + // Parse the response + var collaborators []Collaborator + if err := json.Unmarshal(resp.Body(), &collaborators); err != nil { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some(fmt.Sprintf("Failed to parse collaborators: %s", err)), + }}, + }, nil + } + + // Marshal the response + responseJSON, err := json.Marshal(collaborators) + if err != nil { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some(fmt.Sprintf("Failed to marshal response: %s", err)), + }}, + }, nil + } + + return CallToolResult{ + Content: []Content{{ + Type: ContentTypeText, + Text: some(string(responseJSON)), + }}, + }, nil +} + +type RepositoryDetails struct { + Name string `json:"name"` + FullName string `json:"full_name"` + Description string `json:"description"` + Private bool `json:"private"` + Owner struct { + Login string `json:"login"` + } `json:"owner"` + HTMLURL string `json:"html_url"` + Stargazers int `json:"stargazers_count"` + Watchers int `json:"watchers_count"` + Forks int `json:"forks_count"` + OpenIssues int `json:"open_issues_count"` + DefaultBranch string `json:"default_branch"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + PushedAt string `json:"pushed_at"` +} + +func reposGetDetails(apiKey string, owner, repo string) (CallToolResult, error) { + url := fmt.Sprintf("https://api.github.com/repos/%s/%s", owner, repo) + pdk.Log(pdk.LogDebug, fmt.Sprint("Fetching repository details: ", url)) + + req := pdk.NewHTTPRequest(pdk.MethodGet, url) + req.SetHeader("Authorization", fmt.Sprint("token ", apiKey)) + req.SetHeader("Accept", "application/vnd.github+json") + req.SetHeader("User-Agent", "github-mcpx-servlet") + + resp := req.Send() + if resp.Status() != 200 { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some(fmt.Sprintf("Failed to fetch repository details: %d %s", resp.Status(), string(resp.Body()))), + }}, + }, nil + } + + var repoDetails RepositoryDetails + if err := json.Unmarshal(resp.Body(), &repoDetails); err != nil { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some(fmt.Sprintf("Failed to parse repository details: %s", err)), + }}, + }, nil + } + + responseJSON, err := json.Marshal(repoDetails) + if err != nil { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some(fmt.Sprintf("Failed to marshal response: %s", err)), + }}, + }, nil + } + + return CallToolResult{ + Content: []Content{{ + Type: ContentTypeText, + Text: some(string(responseJSON)), + }}, + }, nil +} + +func reposList(apiKey string, username string, args map[string]interface{}) (CallToolResult, error) { + baseURL := fmt.Sprintf("https://api.github.com/users/%s/repos", username) + params := make([]string, 0) + + // Optional parameters + if value, ok := args["type"].(string); ok && value != "" { + params = append(params, fmt.Sprintf("type=%s", value)) + } + if value, ok := args["sort"].(string); ok && value != "" { + params = append(params, fmt.Sprintf("sort=%s", value)) + } + if value, ok := args["direction"].(string); ok && value != "" { + params = append(params, fmt.Sprintf("direction=%s", value)) + } + + // Pagination parameters + perPage := 30 // Default value + if value, ok := args["per_page"].(float64); ok { + if value > 100 { + perPage = 100 // Max value + } else if value > 0 { + perPage = int(value) + } + } + params = append(params, fmt.Sprintf("per_page=%d", perPage)) + + page := 1 // Default value + if value, ok := args["page"].(float64); ok && value > 0 { + page = int(value) + } + params = append(params, fmt.Sprintf("page=%d", page)) + + // Build final URL + url := baseURL + if len(params) > 0 { + url = fmt.Sprintf("%s?%s", baseURL, strings.Join(params, "&")) + } + + pdk.Log(pdk.LogDebug, fmt.Sprint("Fetching repositories: ", url)) + + // Make request + req := pdk.NewHTTPRequest(pdk.MethodGet, url) + req.SetHeader("Authorization", fmt.Sprint("token ", apiKey)) + req.SetHeader("Accept", "application/vnd.github+json") + req.SetHeader("User-Agent", "github-mcpx-servlet") + + resp := req.Send() + if resp.Status() != 200 { + return CallToolResult{ + IsError: some(true), + Content: []Content{{ + Type: ContentTypeText, + Text: some(fmt.Sprintf("Failed to fetch repositories: %d %s", resp.Status(), string(resp.Body()))), + }}, + }, nil + } + + return CallToolResult{ + Content: []Content{{ + Type: ContentTypeText, + Text: some(string(resp.Body())), + }}, + }, nil +}