Skip to content
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

feat: add list issues/prs to github servlet #50

Merged
merged 2 commits into from
Jan 28, 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
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
Loading