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
}