diff --git a/pkg/github/__toolsnaps__/search_code.snap b/pkg/github/__toolsnaps__/search_code.snap index 4ef40c5f8..aebd432bf 100644 --- a/pkg/github/__toolsnaps__/search_code.snap +++ b/pkg/github/__toolsnaps__/search_code.snap @@ -1,43 +1,43 @@ { "annotations": { - "title": "Search code", - "readOnlyHint": true + "readOnlyHint": true, + "title": "Search code" }, "description": "Fast and precise code search across ALL GitHub repositories using GitHub's native search engine. Best for finding exact symbols, functions, classes, or specific code patterns.", "inputSchema": { + "type": "object", + "required": [ + "query" + ], "properties": { "order": { + "type": "string", "description": "Sort order for results", "enum": [ "asc", "desc" - ], - "type": "string" + ] }, "page": { + "type": "number", "description": "Page number for pagination (min 1)", - "minimum": 1, - "type": "number" + "minimum": 1 }, "perPage": { + "type": "number", "description": "Results per page for pagination (min 1, max 100)", - "maximum": 100, "minimum": 1, - "type": "number" + "maximum": 100 }, "query": { - "description": "Search query using GitHub's powerful code search syntax. Examples: 'content:Skill language:Java org:github', 'NOT is:archived language:Python OR language:go', 'repo:github/github-mcp-server'. Supports exact matching, language filters, path filters, and more.", - "type": "string" + "type": "string", + "description": "Search query using GitHub's powerful code search syntax. Examples: 'content:Skill language:Java org:github', 'NOT is:archived language:Python OR language:go', 'repo:github/github-mcp-server'. Supports exact matching, language filters, path filters, and more." }, "sort": { - "description": "Sort field ('indexed' only)", - "type": "string" + "type": "string", + "description": "Sort field ('indexed' only)" } - }, - "required": [ - "query" - ], - "type": "object" + } }, "name": "search_code" } \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/search_orgs.snap b/pkg/github/__toolsnaps__/search_orgs.snap new file mode 100644 index 000000000..36eb948ae --- /dev/null +++ b/pkg/github/__toolsnaps__/search_orgs.snap @@ -0,0 +1,48 @@ +{ + "annotations": { + "readOnlyHint": true, + "title": "Search organizations" + }, + "description": "Find GitHub organizations by name, location, or other organization metadata. Ideal for discovering companies, open source foundations, or teams.", + "inputSchema": { + "type": "object", + "required": [ + "query" + ], + "properties": { + "order": { + "type": "string", + "description": "Sort order", + "enum": [ + "asc", + "desc" + ] + }, + "page": { + "type": "number", + "description": "Page number for pagination (min 1)", + "minimum": 1 + }, + "perPage": { + "type": "number", + "description": "Results per page for pagination (min 1, max 100)", + "minimum": 1, + "maximum": 100 + }, + "query": { + "type": "string", + "description": "Organization search query. Examples: 'microsoft', 'location:california', 'created:\u003e=2025-01-01'. Search is automatically scoped to type:org." + }, + "sort": { + "type": "string", + "description": "Sort field by category", + "enum": [ + "followers", + "repositories", + "joined" + ] + } + } + }, + "name": "search_orgs" +} \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/search_repositories.snap b/pkg/github/__toolsnaps__/search_repositories.snap index 99828380e..881bc3816 100644 --- a/pkg/github/__toolsnaps__/search_repositories.snap +++ b/pkg/github/__toolsnaps__/search_repositories.snap @@ -1,54 +1,54 @@ { "annotations": { - "title": "Search repositories", - "readOnlyHint": true + "readOnlyHint": true, + "title": "Search repositories" }, "description": "Find GitHub repositories by name, description, readme, topics, or other metadata. Perfect for discovering projects, finding examples, or locating specific repositories across GitHub.", "inputSchema": { + "type": "object", + "required": [ + "query" + ], "properties": { "minimal_output": { - "default": true, + "type": "boolean", "description": "Return minimal repository information (default: true). When false, returns full GitHub API repository objects.", - "type": "boolean" + "default": true }, "order": { + "type": "string", "description": "Sort order", "enum": [ "asc", "desc" - ], - "type": "string" + ] }, "page": { + "type": "number", "description": "Page number for pagination (min 1)", - "minimum": 1, - "type": "number" + "minimum": 1 }, "perPage": { + "type": "number", "description": "Results per page for pagination (min 1, max 100)", - "maximum": 100, "minimum": 1, - "type": "number" + "maximum": 100 }, "query": { - "description": "Repository search query. Examples: 'machine learning in:name stars:\u003e1000 language:python', 'topic:react', 'user:facebook'. Supports advanced search syntax for precise filtering.", - "type": "string" + "type": "string", + "description": "Repository search query. Examples: 'machine learning in:name stars:\u003e1000 language:python', 'topic:react', 'user:facebook'. Supports advanced search syntax for precise filtering." }, "sort": { + "type": "string", "description": "Sort repositories by field, defaults to best match", "enum": [ "stars", "forks", "help-wanted-issues", "updated" - ], - "type": "string" + ] } - }, - "required": [ - "query" - ], - "type": "object" + } }, "name": "search_repositories" } \ No newline at end of file diff --git a/pkg/github/__toolsnaps__/search_users.snap b/pkg/github/__toolsnaps__/search_users.snap index 73ff7a43c..293107696 100644 --- a/pkg/github/__toolsnaps__/search_users.snap +++ b/pkg/github/__toolsnaps__/search_users.snap @@ -1,48 +1,48 @@ { "annotations": { - "title": "Search users", - "readOnlyHint": true + "readOnlyHint": true, + "title": "Search users" }, "description": "Find GitHub users by username, real name, or other profile information. Useful for locating developers, contributors, or team members.", "inputSchema": { + "type": "object", + "required": [ + "query" + ], "properties": { "order": { + "type": "string", "description": "Sort order", "enum": [ "asc", "desc" - ], - "type": "string" + ] }, "page": { + "type": "number", "description": "Page number for pagination (min 1)", - "minimum": 1, - "type": "number" + "minimum": 1 }, "perPage": { + "type": "number", "description": "Results per page for pagination (min 1, max 100)", - "maximum": 100, "minimum": 1, - "type": "number" + "maximum": 100 }, "query": { - "description": "User search query. Examples: 'john smith', 'location:seattle', 'followers:\u003e100'. Search is automatically scoped to type:user.", - "type": "string" + "type": "string", + "description": "User search query. Examples: 'john smith', 'location:seattle', 'followers:\u003e100'. Search is automatically scoped to type:user." }, "sort": { + "type": "string", "description": "Sort users by number of followers or repositories, or when the person joined GitHub.", "enum": [ "followers", "repositories", "joined" - ], - "type": "string" + ] } - }, - "required": [ - "query" - ], - "type": "object" + } }, "name": "search_users" } \ No newline at end of file diff --git a/pkg/github/search.go b/pkg/github/search.go index c909c10ca..cffd0bf15 100644 --- a/pkg/github/search.go +++ b/pkg/github/search.go @@ -1,5 +1,3 @@ -//go:build ignore - package github import ( @@ -7,61 +5,74 @@ import ( "encoding/json" "fmt" "io" + "net/http" ghErrors "github.com/github/github-mcp-server/pkg/errors" "github.com/github/github-mcp-server/pkg/translations" + "github.com/github/github-mcp-server/pkg/utils" "github.com/google/go-github/v79/github" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" ) // SearchRepositories creates a tool to search for GitHub repositories. -func SearchRepositories(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { - return mcp.NewTool("search_repositories", - mcp.WithDescription(t("TOOL_SEARCH_REPOSITORIES_DESCRIPTION", "Find GitHub repositories by name, description, readme, topics, or other metadata. Perfect for discovering projects, finding examples, or locating specific repositories across GitHub.")), +func SearchRepositories(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { + schema := &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "query": { + Type: "string", + Description: "Repository search query. Examples: 'machine learning in:name stars:>1000 language:python', 'topic:react', 'user:facebook'. Supports advanced search syntax for precise filtering.", + }, + "sort": { + Type: "string", + Description: "Sort repositories by field, defaults to best match", + Enum: []any{"stars", "forks", "help-wanted-issues", "updated"}, + }, + "order": { + Type: "string", + Description: "Sort order", + Enum: []any{"asc", "desc"}, + }, + "minimal_output": { + Type: "boolean", + Description: "Return minimal repository information (default: true). When false, returns full GitHub API repository objects.", + Default: json.RawMessage(`true`), + }, + }, + Required: []string{"query"}, + } + WithPagination(schema) - mcp.WithToolAnnotation(mcp.ToolAnnotation{ + return mcp.Tool{ + Name: "search_repositories", + Description: t("TOOL_SEARCH_REPOSITORIES_DESCRIPTION", "Find GitHub repositories by name, description, readme, topics, or other metadata. Perfect for discovering projects, finding examples, or locating specific repositories across GitHub."), + Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_SEARCH_REPOSITORIES_USER_TITLE", "Search repositories"), - ReadOnlyHint: ToBoolPtr(true), - }), - mcp.WithString("query", - mcp.Required(), - mcp.Description("Repository search query. Examples: 'machine learning in:name stars:>1000 language:python', 'topic:react', 'user:facebook'. Supports advanced search syntax for precise filtering."), - ), - mcp.WithString("sort", - mcp.Description("Sort repositories by field, defaults to best match"), - mcp.Enum("stars", "forks", "help-wanted-issues", "updated"), - ), - mcp.WithString("order", - mcp.Description("Sort order"), - mcp.Enum("asc", "desc"), - ), - mcp.WithBoolean("minimal_output", - mcp.Description("Return minimal repository information (default: true). When false, returns full GitHub API repository objects."), - mcp.DefaultBool(true), - ), - WithPagination(), - ), - func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - query, err := RequiredParam[string](request, "query") + ReadOnlyHint: true, + }, + InputSchema: schema, + }, + func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + query, err := RequiredParam[string](args, "query") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } - sort, err := OptionalParam[string](request, "sort") + sort, err := OptionalParam[string](args, "sort") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } - order, err := OptionalParam[string](request, "order") + order, err := OptionalParam[string](args, "order") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } - pagination, err := OptionalPaginationParams(request) + pagination, err := OptionalPaginationParams(args) if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } - minimalOutput, err := OptionalBoolParamWithDefault(request, "minimal_output", true) + minimalOutput, err := OptionalBoolParamWithDefault(args, "minimal_output", true) if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } opts := &github.SearchOptions{ Sort: sort, @@ -74,7 +85,7 @@ func SearchRepositories(getClient GetClientFn, t translations.TranslationHelperF client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil } result, resp, err := client.Search.Repositories(ctx, query, opts) if err != nil { @@ -82,16 +93,16 @@ func SearchRepositories(getClient GetClientFn, t translations.TranslationHelperF fmt.Sprintf("failed to search repositories with query '%s'", query), resp, err, - ), nil + ), nil, nil } defer func() { _ = resp.Body.Close() }() - if resp.StatusCode != 200 { + if resp.StatusCode != http.StatusOK { body, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) + return utils.NewToolResultErrorFromErr("failed to read response body", err), nil, nil } - return mcp.NewToolResultError(fmt.Sprintf("failed to search repositories: %s", string(body))), nil + return utils.NewToolResultError(fmt.Sprintf("failed to search repositories: %s", string(body))), nil, nil } // Return either minimal or full response based on parameter @@ -136,56 +147,67 @@ func SearchRepositories(getClient GetClientFn, t translations.TranslationHelperF r, err = json.Marshal(minimalResult) if err != nil { - return nil, fmt.Errorf("failed to marshal minimal response: %w", err) + return utils.NewToolResultErrorFromErr("failed to marshal minimal response", err), nil, nil } } else { r, err = json.Marshal(result) if err != nil { - return nil, fmt.Errorf("failed to marshal full response: %w", err) + return utils.NewToolResultErrorFromErr("failed to marshal full response", err), nil, nil } } - return mcp.NewToolResultText(string(r)), nil + return utils.NewToolResultText(string(r)), nil, nil } } // SearchCode creates a tool to search for code across GitHub repositories. -func SearchCode(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { - return mcp.NewTool("search_code", - mcp.WithDescription(t("TOOL_SEARCH_CODE_DESCRIPTION", "Fast and precise code search across ALL GitHub repositories using GitHub's native search engine. Best for finding exact symbols, functions, classes, or specific code patterns.")), - mcp.WithToolAnnotation(mcp.ToolAnnotation{ +func SearchCode(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { + schema := &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "query": { + Type: "string", + Description: "Search query using GitHub's powerful code search syntax. Examples: 'content:Skill language:Java org:github', 'NOT is:archived language:Python OR language:go', 'repo:github/github-mcp-server'. Supports exact matching, language filters, path filters, and more.", + }, + "sort": { + Type: "string", + Description: "Sort field ('indexed' only)", + }, + "order": { + Type: "string", + Description: "Sort order for results", + Enum: []any{"asc", "desc"}, + }, + }, + Required: []string{"query"}, + } + WithPagination(schema) + + return mcp.Tool{ + Name: "search_code", + Description: t("TOOL_SEARCH_CODE_DESCRIPTION", "Fast and precise code search across ALL GitHub repositories using GitHub's native search engine. Best for finding exact symbols, functions, classes, or specific code patterns."), + Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_SEARCH_CODE_USER_TITLE", "Search code"), - ReadOnlyHint: ToBoolPtr(true), - }), - mcp.WithString("query", - mcp.Required(), - mcp.Description("Search query using GitHub's powerful code search syntax. Examples: 'content:Skill language:Java org:github', 'NOT is:archived language:Python OR language:go', 'repo:github/github-mcp-server'. Supports exact matching, language filters, path filters, and more."), - ), - mcp.WithString("sort", - mcp.Description("Sort field ('indexed' only)"), - ), - mcp.WithString("order", - mcp.Description("Sort order for results"), - mcp.Enum("asc", "desc"), - ), - WithPagination(), - ), - func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - query, err := RequiredParam[string](request, "query") + ReadOnlyHint: true, + }, + InputSchema: schema, + }, + func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + query, err := RequiredParam[string](args, "query") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } - sort, err := OptionalParam[string](request, "sort") + sort, err := OptionalParam[string](args, "sort") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } - order, err := OptionalParam[string](request, "order") + order, err := OptionalParam[string](args, "order") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } - pagination, err := OptionalPaginationParams(request) + pagination, err := OptionalPaginationParams(args) if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } opts := &github.SearchOptions{ @@ -199,7 +221,7 @@ func SearchCode(getClient GetClientFn, t translations.TranslationHelperFunc) (to client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil } result, resp, err := client.Search.Code(ctx, query, opts) @@ -208,44 +230,44 @@ func SearchCode(getClient GetClientFn, t translations.TranslationHelperFunc) (to fmt.Sprintf("failed to search code with query '%s'", query), resp, err, - ), nil + ), nil, nil } defer func() { _ = resp.Body.Close() }() - if resp.StatusCode != 200 { + if resp.StatusCode != http.StatusOK { body, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) + return utils.NewToolResultErrorFromErr("failed to read response body", err), nil, nil } - return mcp.NewToolResultError(fmt.Sprintf("failed to search code: %s", string(body))), nil + return utils.NewToolResultError(fmt.Sprintf("failed to search code: %s", string(body))), nil, nil } r, err := json.Marshal(result) if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) + return utils.NewToolResultErrorFromErr("failed to marshal response", err), nil, nil } - return mcp.NewToolResultText(string(r)), nil + return utils.NewToolResultText(string(r)), nil, nil } } -func userOrOrgHandler(accountType string, getClient GetClientFn) server.ToolHandlerFunc { - return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - query, err := RequiredParam[string](request, "query") +func userOrOrgHandler(accountType string, getClient GetClientFn) mcp.ToolHandlerFor[map[string]any, any] { + return func(ctx context.Context, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + query, err := RequiredParam[string](args, "query") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } - sort, err := OptionalParam[string](request, "sort") + sort, err := OptionalParam[string](args, "sort") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } - order, err := OptionalParam[string](request, "order") + order, err := OptionalParam[string](args, "order") if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } - pagination, err := OptionalPaginationParams(request) + pagination, err := OptionalPaginationParams(args) if err != nil { - return mcp.NewToolResultError(err.Error()), nil + return utils.NewToolResultError(err.Error()), nil, nil } opts := &github.SearchOptions{ @@ -259,7 +281,7 @@ func userOrOrgHandler(accountType string, getClient GetClientFn) server.ToolHand client, err := getClient(ctx) if err != nil { - return nil, fmt.Errorf("failed to get GitHub client: %w", err) + return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil } searchQuery := query @@ -272,16 +294,16 @@ func userOrOrgHandler(accountType string, getClient GetClientFn) server.ToolHand fmt.Sprintf("failed to search %ss with query '%s'", accountType, query), resp, err, - ), nil + ), nil, nil } defer func() { _ = resp.Body.Close() }() - if resp.StatusCode != 200 { + if resp.StatusCode != http.StatusOK { body, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) + return utils.NewToolResultErrorFromErr("failed to read response body", err), nil, nil } - return mcp.NewToolResultError(fmt.Sprintf("failed to search %ss: %s", accountType, string(body))), nil + return utils.NewToolResultError(fmt.Sprintf("failed to search %ss: %s", accountType, string(body))), nil, nil } minimalUsers := make([]MinimalUser, 0, len(result.Users)) @@ -311,57 +333,78 @@ func userOrOrgHandler(accountType string, getClient GetClientFn) server.ToolHand r, err := json.Marshal(minimalResp) if err != nil { - return nil, fmt.Errorf("failed to marshal response: %w", err) + return utils.NewToolResultErrorFromErr("failed to marshal response", err), nil, nil } - return mcp.NewToolResultText(string(r)), nil + return utils.NewToolResultText(string(r)), nil, nil } } // SearchUsers creates a tool to search for GitHub users. -func SearchUsers(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { - return mcp.NewTool("search_users", - mcp.WithDescription(t("TOOL_SEARCH_USERS_DESCRIPTION", "Find GitHub users by username, real name, or other profile information. Useful for locating developers, contributors, or team members.")), - mcp.WithToolAnnotation(mcp.ToolAnnotation{ +func SearchUsers(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { + schema := &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "query": { + Type: "string", + Description: "User search query. Examples: 'john smith', 'location:seattle', 'followers:>100'. Search is automatically scoped to type:user.", + }, + "sort": { + Type: "string", + Description: "Sort users by number of followers or repositories, or when the person joined GitHub.", + Enum: []any{"followers", "repositories", "joined"}, + }, + "order": { + Type: "string", + Description: "Sort order", + Enum: []any{"asc", "desc"}, + }, + }, + Required: []string{"query"}, + } + WithPagination(schema) + + return mcp.Tool{ + Name: "search_users", + Description: t("TOOL_SEARCH_USERS_DESCRIPTION", "Find GitHub users by username, real name, or other profile information. Useful for locating developers, contributors, or team members."), + Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_SEARCH_USERS_USER_TITLE", "Search users"), - ReadOnlyHint: ToBoolPtr(true), - }), - mcp.WithString("query", - mcp.Required(), - mcp.Description("User search query. Examples: 'john smith', 'location:seattle', 'followers:>100'. Search is automatically scoped to type:user."), - ), - mcp.WithString("sort", - mcp.Description("Sort users by number of followers or repositories, or when the person joined GitHub."), - mcp.Enum("followers", "repositories", "joined"), - ), - mcp.WithString("order", - mcp.Description("Sort order"), - mcp.Enum("asc", "desc"), - ), - WithPagination(), - ), userOrOrgHandler("user", getClient) + ReadOnlyHint: true, + }, + InputSchema: schema, + }, userOrOrgHandler("user", getClient) } // SearchOrgs creates a tool to search for GitHub organizations. -func SearchOrgs(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { - return mcp.NewTool("search_orgs", - mcp.WithDescription(t("TOOL_SEARCH_ORGS_DESCRIPTION", "Find GitHub organizations by name, location, or other organization metadata. Ideal for discovering companies, open source foundations, or teams.")), +func SearchOrgs(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[map[string]any, any]) { + schema := &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "query": { + Type: "string", + Description: "Organization search query. Examples: 'microsoft', 'location:california', 'created:>=2025-01-01'. Search is automatically scoped to type:org.", + }, + "sort": { + Type: "string", + Description: "Sort field by category", + Enum: []any{"followers", "repositories", "joined"}, + }, + "order": { + Type: "string", + Description: "Sort order", + Enum: []any{"asc", "desc"}, + }, + }, + Required: []string{"query"}, + } + WithPagination(schema) - mcp.WithToolAnnotation(mcp.ToolAnnotation{ + return mcp.Tool{ + Name: "search_orgs", + Description: t("TOOL_SEARCH_ORGS_DESCRIPTION", "Find GitHub organizations by name, location, or other organization metadata. Ideal for discovering companies, open source foundations, or teams."), + Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_SEARCH_ORGS_USER_TITLE", "Search organizations"), - ReadOnlyHint: ToBoolPtr(true), - }), - mcp.WithString("query", - mcp.Required(), - mcp.Description("Organization search query. Examples: 'microsoft', 'location:california', 'created:>=2025-01-01'. Search is automatically scoped to type:org."), - ), - mcp.WithString("sort", - mcp.Description("Sort field by category"), - mcp.Enum("followers", "repositories", "joined"), - ), - mcp.WithString("order", - mcp.Description("Sort order"), - mcp.Enum("asc", "desc"), - ), - WithPagination(), - ), userOrOrgHandler("org", getClient) + ReadOnlyHint: true, + }, + InputSchema: schema, + }, userOrOrgHandler("org", getClient) } diff --git a/pkg/github/search_test.go b/pkg/github/search_test.go index d823e2070..0b923edcd 100644 --- a/pkg/github/search_test.go +++ b/pkg/github/search_test.go @@ -1,5 +1,3 @@ -//go:build ignore - package github import ( @@ -11,6 +9,7 @@ import ( "github.com/github/github-mcp-server/internal/toolsnaps" "github.com/github/github-mcp-server/pkg/translations" "github.com/google/go-github/v79/github" + "github.com/google/jsonschema-go/jsonschema" "github.com/migueleliasweb/go-github-mock/src/mock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -24,12 +23,15 @@ func Test_SearchRepositories(t *testing.T) { assert.Equal(t, "search_repositories", tool.Name) assert.NotEmpty(t, tool.Description) - assert.Contains(t, tool.InputSchema.Properties, "query") - assert.Contains(t, tool.InputSchema.Properties, "sort") - assert.Contains(t, tool.InputSchema.Properties, "order") - assert.Contains(t, tool.InputSchema.Properties, "page") - assert.Contains(t, tool.InputSchema.Properties, "perPage") - assert.ElementsMatch(t, tool.InputSchema.Required, []string{"query"}) + + schema, ok := tool.InputSchema.(*jsonschema.Schema) + require.True(t, ok, "InputSchema should be *jsonschema.Schema") + assert.Contains(t, schema.Properties, "query") + assert.Contains(t, schema.Properties, "sort") + assert.Contains(t, schema.Properties, "order") + assert.Contains(t, schema.Properties, "page") + assert.Contains(t, schema.Properties, "perPage") + assert.ElementsMatch(t, schema.Required, []string{"query"}) // Setup mock search results mockSearchResult := &github.RepositoriesSearchResult{ @@ -138,7 +140,7 @@ func Test_SearchRepositories(t *testing.T) { request := createMCPRequest(tc.requestArgs) // Call handler - result, err := handler(context.Background(), request) + result, _, err := handler(context.Background(), &request, tc.requestArgs) // Verify results if tc.expectError { @@ -205,12 +207,14 @@ func Test_SearchRepositories_FullOutput(t *testing.T) { client := github.NewClient(mockedClient) _, handlerTest := SearchRepositories(stubGetClientFn(client), translations.NullTranslationHelper) - request := createMCPRequest(map[string]interface{}{ + args := map[string]interface{}{ "query": "golang test", "minimal_output": false, - }) + } - result, err := handlerTest(context.Background(), request) + request := createMCPRequest(args) + + result, _, err := handlerTest(context.Background(), &request, args) require.NoError(t, err) require.False(t, result.IsError) @@ -238,12 +242,15 @@ func Test_SearchCode(t *testing.T) { assert.Equal(t, "search_code", tool.Name) assert.NotEmpty(t, tool.Description) - assert.Contains(t, tool.InputSchema.Properties, "query") - assert.Contains(t, tool.InputSchema.Properties, "sort") - assert.Contains(t, tool.InputSchema.Properties, "order") - assert.Contains(t, tool.InputSchema.Properties, "perPage") - assert.Contains(t, tool.InputSchema.Properties, "page") - assert.ElementsMatch(t, tool.InputSchema.Required, []string{"query"}) + + schema, ok := tool.InputSchema.(*jsonschema.Schema) + require.True(t, ok, "InputSchema should be *jsonschema.Schema") + assert.Contains(t, schema.Properties, "query") + assert.Contains(t, schema.Properties, "sort") + assert.Contains(t, schema.Properties, "order") + assert.Contains(t, schema.Properties, "perPage") + assert.Contains(t, schema.Properties, "page") + assert.ElementsMatch(t, schema.Required, []string{"query"}) // Setup mock search results mockSearchResult := &github.CodeSearchResult{ @@ -350,7 +357,7 @@ func Test_SearchCode(t *testing.T) { request := createMCPRequest(tc.requestArgs) // Call handler - result, err := handler(context.Background(), request) + result, _, err := handler(context.Background(), &request, tc.requestArgs) // Verify results if tc.expectError { @@ -393,12 +400,15 @@ func Test_SearchUsers(t *testing.T) { assert.Equal(t, "search_users", tool.Name) assert.NotEmpty(t, tool.Description) - assert.Contains(t, tool.InputSchema.Properties, "query") - assert.Contains(t, tool.InputSchema.Properties, "sort") - assert.Contains(t, tool.InputSchema.Properties, "order") - assert.Contains(t, tool.InputSchema.Properties, "perPage") - assert.Contains(t, tool.InputSchema.Properties, "page") - assert.ElementsMatch(t, tool.InputSchema.Required, []string{"query"}) + + schema, ok := tool.InputSchema.(*jsonschema.Schema) + require.True(t, ok, "InputSchema should be *jsonschema.Schema") + assert.Contains(t, schema.Properties, "query") + assert.Contains(t, schema.Properties, "sort") + assert.Contains(t, schema.Properties, "order") + assert.Contains(t, schema.Properties, "perPage") + assert.Contains(t, schema.Properties, "page") + assert.ElementsMatch(t, schema.Required, []string{"query"}) // Setup mock search results mockSearchResult := &github.UsersSearchResult{ @@ -544,7 +554,7 @@ func Test_SearchUsers(t *testing.T) { request := createMCPRequest(tc.requestArgs) // Call handler - result, err := handler(context.Background(), request) + result, _, err := handler(context.Background(), &request, tc.requestArgs) // Verify results if tc.expectError { @@ -585,14 +595,19 @@ func Test_SearchOrgs(t *testing.T) { mockClient := github.NewClient(nil) tool, _ := SearchOrgs(stubGetClientFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) + assert.Equal(t, "search_orgs", tool.Name) assert.NotEmpty(t, tool.Description) - assert.Contains(t, tool.InputSchema.Properties, "query") - assert.Contains(t, tool.InputSchema.Properties, "sort") - assert.Contains(t, tool.InputSchema.Properties, "order") - assert.Contains(t, tool.InputSchema.Properties, "perPage") - assert.Contains(t, tool.InputSchema.Properties, "page") - assert.ElementsMatch(t, tool.InputSchema.Required, []string{"query"}) + + schema, ok := tool.InputSchema.(*jsonschema.Schema) + require.True(t, ok, "InputSchema should be *jsonschema.Schema") + assert.Contains(t, schema.Properties, "query") + assert.Contains(t, schema.Properties, "sort") + assert.Contains(t, schema.Properties, "order") + assert.Contains(t, schema.Properties, "perPage") + assert.Contains(t, schema.Properties, "page") + assert.ElementsMatch(t, schema.Required, []string{"query"}) // Setup mock search results mockSearchResult := &github.UsersSearchResult{ @@ -711,7 +726,7 @@ func Test_SearchOrgs(t *testing.T) { request := createMCPRequest(tc.requestArgs) // Call handler - result, err := handler(context.Background(), request) + result, _, err := handler(context.Background(), &request, tc.requestArgs) // Verify results if tc.expectError { diff --git a/pkg/github/tools.go b/pkg/github/tools.go index 9ef0987b8..c2100b8aa 100644 --- a/pkg/github/tools.go +++ b/pkg/github/tools.go @@ -166,19 +166,19 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG // Define all available features with their default state (disabled) // Create toolsets repos := toolsets.NewToolset(ToolsetMetadataRepos.ID, ToolsetMetadataRepos.Description). - // AddReadTools( - // toolsets.NewServerTool(SearchRepositories(getClient, t)), - // toolsets.NewServerTool(GetFileContents(getClient, getRawClient, t)), - // toolsets.NewServerTool(ListCommits(getClient, t)), - // toolsets.NewServerTool(SearchCode(getClient, t)), - // toolsets.NewServerTool(GetCommit(getClient, t)), - // toolsets.NewServerTool(ListBranches(getClient, t)), - // toolsets.NewServerTool(ListTags(getClient, t)), - // toolsets.NewServerTool(GetTag(getClient, t)), - // toolsets.NewServerTool(ListReleases(getClient, t)), - // toolsets.NewServerTool(GetLatestRelease(getClient, t)), - // toolsets.NewServerTool(GetReleaseByTag(getClient, t)), - // ). + AddReadTools( + toolsets.NewServerTool(SearchRepositories(getClient, t)), + // toolsets.NewServerTool(GetFileContents(getClient, getRawClient, t)), + // toolsets.NewServerTool(ListCommits(getClient, t)), + toolsets.NewServerTool(SearchCode(getClient, t)), + // toolsets.NewServerTool(GetCommit(getClient, t)), + // toolsets.NewServerTool(ListBranches(getClient, t)), + // toolsets.NewServerTool(ListTags(getClient, t)), + // toolsets.NewServerTool(GetTag(getClient, t)), + // toolsets.NewServerTool(ListReleases(getClient, t)), + // toolsets.NewServerTool(GetLatestRelease(getClient, t)), + // toolsets.NewServerTool(GetReleaseByTag(getClient, t)), + ). // AddWriteTools( // toolsets.NewServerTool(CreateOrUpdateFile(getClient, t)), // toolsets.NewServerTool(CreateRepository(getClient, t)), @@ -215,14 +215,14 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG toolsets.NewServerPrompt(AssignCodingAgentPrompt(t)), toolsets.NewServerPrompt(IssueToFixWorkflowPrompt(t)), ) - // users := toolsets.NewToolset(ToolsetMetadataUsers.ID, ToolsetMetadataUsers.Description). - // AddReadTools( - // toolsets.NewServerTool(SearchUsers(getClient, t)), - // ) - // orgs := toolsets.NewToolset(ToolsetMetadataOrgs.ID, ToolsetMetadataOrgs.Description). - // AddReadTools( - // toolsets.NewServerTool(SearchOrgs(getClient, t)), - // ) + users := toolsets.NewToolset(ToolsetMetadataUsers.ID, ToolsetMetadataUsers.Description). + AddReadTools( + toolsets.NewServerTool(SearchUsers(getClient, t)), + ) + orgs := toolsets.NewToolset(ToolsetMetadataOrgs.ID, ToolsetMetadataOrgs.Description). + AddReadTools( + toolsets.NewServerTool(SearchOrgs(getClient, t)), + ) pullRequests := toolsets.NewToolset(ToolsetMetadataPullRequests.ID, ToolsetMetadataPullRequests.Description). AddReadTools( toolsets.NewServerTool(PullRequestRead(getClient, t, flags)), @@ -235,7 +235,6 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG toolsets.NewServerTool(CreatePullRequest(getClient, t)), toolsets.NewServerTool(UpdatePullRequest(getClient, getGQLClient, t)), toolsets.NewServerTool(RequestCopilotReview(getClient, t)), - // Reviews toolsets.NewServerTool(PullRequestReviewWrite(getGQLClient, t)), toolsets.NewServerTool(AddCommentToPendingReview(getGQLClient, t)), @@ -363,8 +362,8 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG tsg.AddToolset(repos) tsg.AddToolset(git) tsg.AddToolset(issues) - // tsg.AddToolset(orgs) - // tsg.AddToolset(users) + tsg.AddToolset(orgs) + tsg.AddToolset(users) tsg.AddToolset(pullRequests) tsg.AddToolset(actions) tsg.AddToolset(codeSecurity)