diff --git a/README.md b/README.md index 68742752f..c81e7629b 100644 --- a/README.md +++ b/README.md @@ -272,6 +272,7 @@ The following sets of tools are available (all are on by default): | `dependabot` | Dependabot tools | | `discussions` | GitHub Discussions related tools | | `experiments` | Experimental features that are not considered stable yet | +| `graphql` | GitHub GraphQL API tools for direct query execution | | `issues` | GitHub Issues related tools | | `notifications` | GitHub Notifications related tools | | `orgs` | GitHub Organization related tools | @@ -602,6 +603,16 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description
+Graphql + +- **execute_graphql_query** - Execute GraphQL query + - `query`: The GraphQL query string to execute (string, required) + - `variables`: Variables for the GraphQL query (optional) (object, optional) + +
+ +
+ Issues - **add_issue_comment** - Add comment to issue diff --git a/docs/graphql-tools.md b/docs/graphql-tools.md new file mode 100644 index 000000000..9249441b3 --- /dev/null +++ b/docs/graphql-tools.md @@ -0,0 +1,78 @@ +# GraphQL Tools + +This document describes the GraphQL tools added to the GitHub MCP server that provide direct access to GitHub's GraphQL API. + +## Tools + +### execute_graphql_query + +Executes a GraphQL query against GitHub's API and returns the results. + +#### Parameters + +- `query` (required): The GraphQL query string to execute +- `variables` (optional): Variables for the GraphQL query as a JSON object + +#### Response + +Returns a JSON object with: + +- `query`: The original query string +- `variables`: The variables passed to the query +- `success`: Boolean indicating if the query executed successfully +- `data`: The GraphQL response data (if successful) +- `error`: Error message if execution failed +- `error_type`: Type of execution error (rate_limit, authentication, permission, not_found, execution_error) +- `graphql_errors`: Any GraphQL-specific errors from the response + +#### Example + +```json +{ + "query": "query { viewer { login } }", + "variables": {}, + "success": true, + "data": { + "viewer": { + "login": "username" + } + } +} +``` + +## Implementation Details + +### Execution + +The execution tool uses GitHub's REST client to make raw HTTP requests to the GraphQL endpoint (`/graphql`), allowing for arbitrary GraphQL query execution while maintaining proper authentication and error handling. + +### Error Handling + +The tool provides comprehensive error categorization: + +- **Syntax errors**: Malformed GraphQL syntax +- **Field errors**: References to non-existent fields +- **Type errors**: Type-related validation issues +- **Client errors**: Authentication or connectivity issues +- **Rate limit errors**: API rate limiting +- **Permission errors**: Access denied to resources +- **Not found errors**: Referenced resources don't exist + +## Usage with MCP + +This tool is part of the "graphql" toolset and can be enabled through the dynamic toolset system: + +1. Enable the graphql toolset: `enable_toolset` with name "graphql" +2. Use `execute_graphql_query` to run queries and get results + +## Testing + +The tool includes comprehensive tests covering: + +- Tool definition validation +- Required parameter checking +- Response format validation +- Variable handling +- Error categorization + +Run tests with: `go test -v ./pkg/github -run GraphQL` diff --git a/docs/remote-server.md b/docs/remote-server.md index c36124ecc..8d9dcfd9d 100644 --- a/docs/remote-server.md +++ b/docs/remote-server.md @@ -23,6 +23,7 @@ Below is a table of available toolsets for the remote GitHub MCP Server. Each to | Dependabot | Dependabot tools | https://api.githubcopilot.com/mcp/x/dependabot | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-dependabot&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fdependabot%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/dependabot/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-dependabot&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fdependabot%2Freadonly%22%7D) | | Discussions | GitHub Discussions related tools | https://api.githubcopilot.com/mcp/x/discussions | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-discussions&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fdiscussions%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/discussions/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-discussions&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fdiscussions%2Freadonly%22%7D) | | Experiments | Experimental features that are not considered stable yet | https://api.githubcopilot.com/mcp/x/experiments | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-experiments&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fexperiments%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/experiments/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-experiments&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fexperiments%2Freadonly%22%7D) | +| GraphQL | GitHub GraphQL API tools for direct query execution | https://api.githubcopilot.com/mcp/x/graphql | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-graphql&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fgraphql%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/graphql/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-graphql&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fgraphql%2Freadonly%22%7D) | | Issues | GitHub Issues related tools | https://api.githubcopilot.com/mcp/x/issues | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-issues&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fissues%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/issues/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-issues&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fissues%2Freadonly%22%7D) | | Notifications | GitHub Notifications related tools | https://api.githubcopilot.com/mcp/x/notifications | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-notifications&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fnotifications%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/notifications/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-notifications&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Fnotifications%2Freadonly%22%7D) | | Organizations | GitHub Organization related tools | https://api.githubcopilot.com/mcp/x/orgs | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-orgs&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Forgs%22%7D) | [read-only](https://api.githubcopilot.com/mcp/x/orgs/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-orgs&config=%7B%22type%22%3A%20%22http%22%2C%22url%22%3A%20%22https%3A%2F%2Fapi.githubcopilot.com%2Fmcp%2Fx%2Forgs%2Freadonly%22%7D) | diff --git a/pkg/github/__toolsnaps__/execute_graphql_query.snap b/pkg/github/__toolsnaps__/execute_graphql_query.snap new file mode 100644 index 000000000..0fc44fc6a --- /dev/null +++ b/pkg/github/__toolsnaps__/execute_graphql_query.snap @@ -0,0 +1,25 @@ +{ + "annotations": { + "title": "Execute GraphQL query", + "readOnlyHint": false + }, + "description": "Execute a GraphQL query against GitHub's API and return the results.", + "inputSchema": { + "properties": { + "query": { + "description": "The GraphQL query string to execute", + "type": "string" + }, + "variables": { + "description": "Variables for the GraphQL query (optional)", + "properties": {}, + "type": "object" + } + }, + "required": [ + "query" + ], + "type": "object" + }, + "name": "execute_graphql_query" +} \ No newline at end of file diff --git a/pkg/github/graphql_integration_test.go b/pkg/github/graphql_integration_test.go new file mode 100644 index 000000000..35eca4a2e --- /dev/null +++ b/pkg/github/graphql_integration_test.go @@ -0,0 +1,73 @@ +package github + +import ( + "context" + "encoding/json" + "net/http" + "testing" + + "github.com/github/github-mcp-server/pkg/translations" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestGraphQLToolsIntegration tests that GraphQL tools can be created and called +func TestGraphQLToolsIntegration(t *testing.T) { + t.Parallel() + + // Create mock clients + mockHTTPClient := &http.Client{} + getClient := stubGetClientFromHTTPFn(mockHTTPClient) + + // Test that we can create execute tool without errors + t.Run("create_tools", func(t *testing.T) { + executeTool, executeHandler := ExecuteGraphQLQuery(getClient, translations.NullTranslationHelper) + + // Verify tool definitions + assert.Equal(t, "execute_graphql_query", executeTool.Name) + assert.NotNil(t, executeHandler) + + // Verify tool schemas have required fields + assert.Contains(t, executeTool.InputSchema.Properties, "query") + assert.Contains(t, executeTool.InputSchema.Properties, "variables") + + // Verify required parameters + assert.Contains(t, executeTool.InputSchema.Required, "query") + }) + + // Test basic invocation of execution tool + t.Run("invoke_execute_tool", func(t *testing.T) { + _, handler := ExecuteGraphQLQuery(getClient, translations.NullTranslationHelper) + + request := createMCPRequest(map[string]any{ + "query": `query { viewer { login } }`, + }) + + result, err := handler(context.Background(), request) + require.NoError(t, err) + require.NotNil(t, result) + + textContent := getTextResult(t, result) + var response map[string]interface{} + err = json.Unmarshal([]byte(textContent.Text), &response) + require.NoError(t, err) + + // Should have basic response structure + assert.Contains(t, response, "query") + assert.Contains(t, response, "variables") + assert.Contains(t, response, "success") + }) + + // Test error handling for missing required parameters + t.Run("error_handling", func(t *testing.T) { + _, executeHandler := ExecuteGraphQLQuery(getClient, translations.NullTranslationHelper) + + emptyRequest := createMCPRequest(map[string]any{}) + + // Execute tool should handle missing query parameter + executeResult, err := executeHandler(context.Background(), emptyRequest) + require.NoError(t, err) + textContent := getTextResult(t, executeResult) + assert.Contains(t, textContent.Text, "query") + }) +} diff --git a/pkg/github/graphql_tools.go b/pkg/github/graphql_tools.go new file mode 100644 index 000000000..038bcb12d --- /dev/null +++ b/pkg/github/graphql_tools.go @@ -0,0 +1,104 @@ +package github + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + "github.com/github/github-mcp-server/pkg/translations" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +// ExecuteGraphQLQuery creates a tool to execute a GraphQL query and return results +func ExecuteGraphQLQuery(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) { + return mcp.NewTool("execute_graphql_query", + mcp.WithDescription(t("TOOL_EXECUTE_GRAPHQL_QUERY_DESCRIPTION", "Execute a GraphQL query against GitHub's API and return the results.")), + mcp.WithToolAnnotation(mcp.ToolAnnotation{ + Title: t("TOOL_EXECUTE_GRAPHQL_QUERY_USER_TITLE", "Execute GraphQL query"), + ReadOnlyHint: ToBoolPtr(false), + }), + mcp.WithString("query", + mcp.Required(), + mcp.Description("The GraphQL query string to execute"), + ), + mcp.WithObject("variables", + mcp.Description("Variables for the GraphQL query (optional)"), + ), + ), + func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + queryStr, err := RequiredParam[string](request, "query") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + variables, _ := OptionalParam[map[string]interface{}](request, "variables") + if variables == nil { + variables = make(map[string]interface{}) + } + + client, err := getClient(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get GitHub client: %w", err) + } + + // Create a GraphQL request payload + graphqlPayload := map[string]interface{}{ + "query": queryStr, + "variables": variables, + } + + // Use the underlying HTTP client to make a raw GraphQL request + req, err := client.NewRequest("POST", "graphql", graphqlPayload) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to create request: %v", err)), nil + } + + // Execute the request + var response map[string]interface{} + _, err = client.Do(ctx, req, &response) + + result := map[string]interface{}{ + "query": queryStr, + "variables": variables, + } + + if err != nil { + // Query execution failed + result["success"] = false + result["error"] = err.Error() + + // Try to categorize the error + errorStr := err.Error() + switch { + case strings.Contains(errorStr, "rate limit"): + result["error_type"] = "rate_limit" + case strings.Contains(errorStr, "unauthorized") || strings.Contains(errorStr, "authentication"): + result["error_type"] = "authentication" + case strings.Contains(errorStr, "permission") || strings.Contains(errorStr, "forbidden"): + result["error_type"] = "permission" + case strings.Contains(errorStr, "not found") || strings.Contains(errorStr, "Could not resolve") || strings.Contains(errorStr, "not exist"): + result["error_type"] = "not_found" + default: + result["error_type"] = "execution_error" + } + } else { + // Query executed successfully + result["success"] = true + result["data"] = response["data"] + + // Include any errors from the GraphQL response + if errors, ok := response["errors"]; ok { + result["graphql_errors"] = errors + } + } + + r, err := json.Marshal(result) + if err != nil { + return nil, fmt.Errorf("failed to marshal response: %w", err) + } + + return mcp.NewToolResultText(string(r)), nil + } +} diff --git a/pkg/github/graphql_tools_test.go b/pkg/github/graphql_tools_test.go new file mode 100644 index 000000000..aae0e88a9 --- /dev/null +++ b/pkg/github/graphql_tools_test.go @@ -0,0 +1,96 @@ +package github + +import ( + "context" + "encoding/json" + "net/http" + "testing" + + "github.com/github/github-mcp-server/internal/toolsnaps" + "github.com/github/github-mcp-server/pkg/translations" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestExecuteGraphQLQuery(t *testing.T) { + t.Parallel() + + // Verify tool definition + mockClient := &http.Client{} + tool, _ := ExecuteGraphQLQuery(stubGetClientFromHTTPFn(mockClient), translations.NullTranslationHelper) + require.NoError(t, toolsnaps.Test(tool.Name, tool)) + + assert.Equal(t, "execute_graphql_query", tool.Name) + assert.NotEmpty(t, tool.Description) + assert.Contains(t, tool.InputSchema.Properties, "query") + assert.Contains(t, tool.InputSchema.Properties, "variables") + assert.ElementsMatch(t, tool.InputSchema.Required, []string{"query"}) + + // Test basic functionality + tests := []struct { + name string + requestArgs map[string]any + }{ + { + name: "basic query structure", + requestArgs: map[string]any{ + "query": `query { viewer { login } }`, + }, + }, + { + name: "query with variables", + requestArgs: map[string]any{ + "query": `query($login: String!) { user(login: $login) { login } }`, + "variables": map[string]any{ + "login": "testuser", + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, handler := ExecuteGraphQLQuery(stubGetClientFromHTTPFn(mockClient), translations.NullTranslationHelper) + + request := createMCPRequest(tt.requestArgs) + result, err := handler(context.Background(), request) + + require.NoError(t, err) + require.NotNil(t, result) + + textContent := getTextResult(t, result) + var response map[string]interface{} + err = json.Unmarshal([]byte(textContent.Text), &response) + require.NoError(t, err) + + // Verify that the response contains the expected fields + assert.Equal(t, tt.requestArgs["query"], response["query"]) + if variables, ok := tt.requestArgs["variables"]; ok { + assert.Equal(t, variables, response["variables"]) + } + + // The response should have either success=true or success=false + _, hasSuccess := response["success"] + assert.True(t, hasSuccess, "Response should have 'success' field") + }) + } +} + +func TestGraphQLToolsRequiredParams(t *testing.T) { + t.Parallel() + + t.Run("ExecuteGraphQLQuery requires query parameter", func(t *testing.T) { + mockClient := &http.Client{} + _, handler := ExecuteGraphQLQuery(stubGetClientFromHTTPFn(mockClient), translations.NullTranslationHelper) + + request := createMCPRequest(map[string]any{}) + result, err := handler(context.Background(), request) + + require.NoError(t, err) + require.NotNil(t, result) + + // Should return an error result for missing required parameter + textContent := getTextResult(t, result) + assert.Contains(t, textContent.Text, "query") + }) +} diff --git a/pkg/github/tools.go b/pkg/github/tools.go index a469b7678..d00b4fa24 100644 --- a/pkg/github/tools.go +++ b/pkg/github/tools.go @@ -152,6 +152,11 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG // Keep experiments alive so the system doesn't error out when it's always enabled experiments := toolsets.NewToolset("experiments", "Experimental features that are not considered stable yet") + graphql := toolsets.NewToolset("graphql", "GitHub GraphQL API tools for direct query execution"). + AddWriteTools( + toolsets.NewServerTool(ExecuteGraphQLQuery(getClient, t)), + ) + contextTools := toolsets.NewToolset("context", "Tools that provide context about the current user and GitHub context you are operating in"). AddReadTools( toolsets.NewServerTool(GetMe(getClient, t)), @@ -171,6 +176,7 @@ func DefaultToolsetGroup(readOnly bool, getClient GetClientFn, getGQLClient GetG tsg.AddToolset(notifications) tsg.AddToolset(experiments) tsg.AddToolset(discussions) + tsg.AddToolset(graphql) return tsg } pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy