Skip to content

Commit

Permalink
feat: add list issues/prs to github servlet (#50)
Browse files Browse the repository at this point in the history
* feat: add list issues/prs to github servlet

* add repo tools
  • Loading branch information
mhmd-azeez authored Jan 28, 2025
1 parent ced32fe commit c3b189f
Show file tree
Hide file tree
Showing 4 changed files with 710 additions and 1 deletion.
149 changes: 149 additions & 0 deletions servlets/github/branches.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"encoding/json"
"fmt"
"strings"

"github.com/extism/go-pdk"
)
Expand All @@ -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",
Expand All @@ -44,6 +65,7 @@ var (

var BranchTools = []ToolDescription{
CreateBranchTool,
ListPullRequestsTool,
CreatePullRequestTool,
}

Expand Down Expand Up @@ -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)
Expand Down
107 changes: 107 additions & 0 deletions servlets/github/issues.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -71,6 +96,7 @@ var (
},
}
IssueTools = []ToolDescription{
ListIssuesTool,
CreateIssueTool,
GetIssueTool,
UpdateIssueTool,
Expand All @@ -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 {
Expand Down
43 changes: 42 additions & 1 deletion servlets/github/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -88,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),
Expand All @@ -101,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
}

Expand Down
Loading

0 comments on commit c3b189f

Please sign in to comment.