#
tokens: 19840/50000 21/21 files
lines: off (toggle) GitHub
raw markdown copy
# Directory Structure

```
├── .gitignore
├── .idea
│   ├── .gitignore
│   ├── encodings.xml
│   ├── misc.xml
│   └── vcs.xml
├── LICENSE
├── pom.xml
├── README.md
└── src
    ├── main
    │   ├── java
    │   │   └── com
    │   │       └── devoxx
    │   │           └── agentic
    │   │               └── github
    │   │                   ├── GitHubMcpApplication.java
    │   │                   └── tools
    │   │                       ├── AbstractToolService.java
    │   │                       ├── BranchService.java
    │   │                       ├── CommitService.java
    │   │                       ├── ContentService.java
    │   │                       ├── GitHubClientFactory.java
    │   │                       ├── GitHubEnv.java
    │   │                       ├── IssueService.java
    │   │                       ├── PullRequestService.java
    │   │                       └── RepositoryService.java
    │   └── resources
    │       ├── application.properties
    │       └── mcp-servers-config.json
    └── test
        └── java
            └── com
                └── devoxx
                    └── agentic
                        └── github
                            ├── ClientStdio.java
                            └── GitHubServiceTest.java
```

# Files

--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------

```
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/

### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
*.iws
*.iml
*.ipr

### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache

### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/

### VS Code ###
.vscode/

### Mac OS ###
.DS_Store
/.idea/
/.env

```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
# GitHub MCP Server

This project implements a Model Context Protocol (MCP) server for GitHub API access. It provides a set of tools that allow LLM agents to interact with GitHub repositories, issues, pull requests, and other GitHub resources.

## Features

The server provides the following GitHub operations:

- **Repository Management**
  - List repositories for the authenticated user
  - Get repository details
  - Search for repositories

- **Issue Management**
  - List issues with filtering options
  - Get detailed information about issues
  - Create new issues
  - Add comments to issues
  - Search for issues

- **Pull Request Management**
  - List pull requests with filtering options
  - Get detailed information about pull requests
  - Create comments on pull requests
  - Merge pull requests

- **Branch Management**
  - List branches in a repository
  - Create new branches

- **Commit Management**
  - Get detailed information about commits
  - List commits with filtering options
  - Search for commits by message

- **Content Management**
  - Get file contents from repositories
  - List directory contents
  - Create or update files
  - Search for code within repositories

These operations are exposed as tools for Large Language Models using the Model Context Protocol (MCP), allowing AI systems to safely interact with GitHub through its API.

### Claude Desktop
<img width="931" alt="GitHubExample" src="https://github.com/user-attachments/assets/aa39e8d3-7443-41c7-888f-640680a3a297" />

### DevoxxGenie IDEA plugin
<img width="894" alt="Screenshot 2025-04-11 at 12 32 58" src="https://github.com/user-attachments/assets/06a66881-0ca6-494e-814d-cafe97e870cc" />


## Getting Started

### Prerequisites

- Java 17 or higher
- Maven 3.6+
- Spring Boot 3.3.6
- Spring AI MCP Server components
- A GitHub account and personal access token

### Building the Project

Build the project using Maven:

```bash
mvn clean package
```

### Running the Server

Run the server using the following command:

```bash
java -jar target/GitHubMCP-1.0-SNAPSHOT.jar
```

The server can use STDIO for communication with MCP clients or can be run as a web server.

## Environment Variables

The GitHub MCP server supports the following environment variables for authentication:

- `GITHUB_TOKEN` or `GITHUB_PERSONAL_ACCESS_TOKEN`: Your GitHub personal access token
- `GITHUB_HOST`: The base URL of your GitHub instance (e.g., `github.com` or `github.mycompany.com` for GitHub Enterprise)
- `GITHUB_REPOSITORY`: Default repository to use if not specified in API calls (e.g., `owner/repo`)

You can set these environment variables when launching the MCP server, and the GitHub services will use them as default values. This allows you to avoid having to provide authentication details with every API call.

## Usage with MCP Clients

### Using with Claude Desktop

Edit your claude_desktop_config.json file with the following:

```json
{
  "mcpServers": {
    "github": {
      "command": "java",
      "args": [
        "-Dspring.ai.mcp.server.stdio=true",
        "-Dspring.main.web-application-type=none",
        "-Dlogging.pattern.console=",
        "-jar",
        "/path/to/GitHubMCP/target/GitHubMCP-1.0-SNAPSHOT.jar"
      ],
      "env": {
        "GITHUB_TOKEN": "your-github-token-here",
        "GITHUB_HOST": "github.com",
        "GITHUB_REPOSITORY": "your-username/your-repository"
      }
    }  
  }
}
```

### Using with DevoxxGenie or similar MCP clients

1. In your MCP client, access the MCP Server configuration screen
2. Configure the server with the following settings:
   - **Name**: `GitHub` (or any descriptive name)
   - **Transport Type**: `STDIO`
   - **Command**: Full path to your Java executable
   - **Arguments**:
     ```
     -Dspring.ai.mcp.server.stdio=true
     -Dspring.main.web-application-type=none
     -Dlogging.pattern.console=
     -jar
     /path/to/GitHubMCP/target/GitHubMCP-1.0-SNAPSHOT.jar
     ```
   - **Environment Variables**:
     ```
     GITHUB_TOKEN=your-github-token-here
     GITHUB_HOST=github.com
     GITHUB_REPOSITORY=your-username/your-repository
     ```

## Security Considerations

When using this server, be aware that:
- You need to provide a GitHub personal access token for authentication
- The LLM agent will have access to create, read, and modify GitHub resources
- Consider running the server with appropriate permissions and in a controlled environment
- Ensure your token has only the minimum required permissions for your use case

## Example Usage

### With Environment Variables

If you've configured the MCP server with environment variables, you can simply ask:

```
Can you get a list of open issues in my GitHub repository?
```

Claude will use the GitHub services with the pre-configured authentication details to fetch the open issues from your GitHub repository and summarize them.

### With Explicit Repository

You can override the default repository by specifying it in your request:

```
Can you list the pull requests for the anthropics/claude-playground repository?
```

The GitHub service will use your authentication token but query the specified repository instead of the default one.

## Available Services

The GitHub MCP server is organized into several service classes, each providing different functionality:

1. **RepositoryService**
   - `listRepositories`: List repositories for the authenticated user
   - `getRepository`: Get detailed information about a specific repository
   - `searchRepositories`: Search for repositories matching a query

2. **IssueService**
   - `listIssues`: List issues for a repository with filtering options
   - `getIssue`: Get detailed information about a specific issue
   - `createIssue`: Create a new issue in a repository
   - `addIssueComment`: Add a comment to an issue
   - `searchIssues`: Search for issues matching a query

3. **PullRequestService**
   - `listPullRequests`: List pull requests for a repository with filtering options
   - `getPullRequest`: Get detailed information about a specific pull request
   - `createPullRequestComment`: Add a comment to a pull request
   - `mergePullRequest`: Merge a pull request with specified merge method

4. **BranchService**
   - `listBranches`: List branches in a repository with filtering options
   - `createBranch`: Create a new branch from a specified reference

5. **CommitService**
   - `getCommitDetails`: Get detailed information about a specific commit
   - `listCommits`: List commits in a repository with filtering options
   - `findCommitByMessage`: Search for commits containing specific text in their messages

6. **ContentService**
   - `getFileContents`: Get the contents of a file in a repository
   - `listDirectoryContents`: List contents of a directory in a repository
   - `createOrUpdateFile`: Create or update a file in a repository
   - `searchCode`: Search for code within repositories

Each service provides methods that can be called by LLM agents through the MCP protocol, allowing them to interact with GitHub in a structured and controlled manner.

## Enterprise GitHub Support

This MCP server supports both GitHub.com and GitHub Enterprise instances. To use with GitHub Enterprise, set the `GITHUB_HOST` environment variable to your enterprise GitHub URL.

## Limitations

- The server requires a valid GitHub personal access token with appropriate permissions
- Rate limiting is subject to GitHub API limits
- Some operations may require specific permissions on the token
- Large repositories or files may encounter performance limitations

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

## License

This project is licensed under the MIT License - see the LICENSE file for details.

```

--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------

```
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="VcsDirectoryMappings">
    <mapping directory="$PROJECT_DIR$" vcs="Git" />
  </component>
</project>
```

--------------------------------------------------------------------------------
/.idea/encodings.xml:
--------------------------------------------------------------------------------

```
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="Encoding">
    <file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
    <file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
  </component>
</project>
```

--------------------------------------------------------------------------------
/src/main/resources/application.properties:
--------------------------------------------------------------------------------

```
# Application settings for GitHub MCP server
# NOTE: You must disable the banner and the console logging 
# to allow the STDIO transport to work !!!
spring.main.banner-mode=off
logging.pattern.console=

# MCP server configuration
spring.ai.mcp.server.name=github-server
spring.ai.mcp.server.version=0.0.1

# spring.ai.mcp.server.stdio=true

# Web server configuration (disable for STDIO mode)
# spring.main.web-application-type=none

# Logging configuration
logging.file.name=./target/github-server.log

```

--------------------------------------------------------------------------------
/src/main/resources/mcp-servers-config.json:
--------------------------------------------------------------------------------

```json
{
  "mcpServers": {
    "devoxx-github": {
      "command": "java",
      "args": [
        "-Dspring.ai.mcp.server.stdio=true",
        "-Dspring.main.web-application-type=none",
        "-Dlogging.pattern.console=",
        "-jar",
        "/Users/stephan/IdeaProjects/GitHubMCP/target/GitHubMCP-1.0-SNAPSHOT.jar"
      ],
      "env": {
        "GITHUB_TOKEN": "your personal access token",
        "GITHUB_HOST": "github.com",
        "GITHUB_REPOSITORY": "your-username/your-repository"
      }
    }
  }
}

```

--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------

```
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="ExternalStorageConfigurationManager" enabled="true" />
  <component name="MavenProjectsManager">
    <option name="originalFiles">
      <list>
        <option value="$PROJECT_DIR$/pom.xml" />
      </list>
    </option>
  </component>
  <component name="ProjectRootManager" version="2" languageLevel="JDK_23" default="true" project-jdk-name="openjdk-23" project-jdk-type="JavaSDK">
    <output url="file://$PROJECT_DIR$/out" />
  </component>
</project>
```

--------------------------------------------------------------------------------
/src/main/java/com/devoxx/agentic/github/tools/GitHubEnv.java:
--------------------------------------------------------------------------------

```java
package com.devoxx.agentic.github.tools;

public record GitHubEnv(String githubToken, String githubHost, String repository) {
    /**
     * Returns whether the environment is set up for GitHub Enterprise
     */
    public boolean isEnterprise() {
        return githubHost != null && !githubHost.isEmpty() && 
               !githubHost.equals("github.com") && !githubHost.equals("https://github.com");
    }
    
    /**
     * Gets the full repository name in owner/repo format
     */
    public String getFullRepository() {
        return repository;
    }
}

```

--------------------------------------------------------------------------------
/src/main/java/com/devoxx/agentic/github/tools/AbstractToolService.java:
--------------------------------------------------------------------------------

```java
package com.devoxx.agentic.github.tools;

import com.fasterxml.jackson.databind.ObjectMapper;

import java.util.HashMap;
import java.util.Map;

public class AbstractToolService {

    protected static final String SUCCESS = "success";
    protected static final String ERROR = "error";

    protected final ObjectMapper mapper = new ObjectMapper();

    protected String errorMessage(String errorMessage) {
        Map<String, Object> result = new HashMap<>();
        result.put(SUCCESS, false);
        result.put(ERROR, errorMessage);
        try {
            return mapper.writeValueAsString(result);
        } catch (Exception ex) {
            return "{\"success\": false, \"error\": \"Failed to serialize error result\"}";
        }
    }

    protected String successMessage(Map<String, Object> result) {
        result.put(SUCCESS, true);
        try {
            return mapper.writeValueAsString(result);
        } catch (Exception ex) {
            return "{\"success\": false, \"error\": \"Failed to serialize success result\"}";
        }
    }
}

```

--------------------------------------------------------------------------------
/src/test/java/com/devoxx/agentic/github/ClientStdio.java:
--------------------------------------------------------------------------------

```java
package com.devoxx.agentic.github;

import io.github.cdimascio.dotenv.Dotenv;
import io.modelcontextprotocol.client.McpClient;
import io.modelcontextprotocol.client.transport.ServerParameters;
import io.modelcontextprotocol.client.transport.StdioClientTransport;
import io.modelcontextprotocol.spec.McpSchema.ListToolsResult;

public class ClientStdio {

    public static void main(String[] args) {
        var stdioParams = ServerParameters.builder("java")
                .args("-Dspring.ai.mcp.server.stdio=true", 
                      "-Dspring.main.web-application-type=none",
                      "-Dlogging.pattern.console=", 
                      "-jar",
                      "/Users/stephan/IdeaProjects/GitHubMCP/target/GitHubMCP-1.0-SNAPSHOT.jar")
                .addEnvVar("GITHUB_TOKEN", Dotenv.load().get("GITHUB_TOKEN"))
                .addEnvVar("GITHUB_HOST", "github.com")
                .addEnvVar("GITHUB_REPOSITORY", "stephan-dowding/mcp-examples")
                .build();

        var transport = new StdioClientTransport(stdioParams);
        var client = McpClient.sync(transport).build();

        client.initialize();

        // List and demonstrate tools
        ListToolsResult toolsList = client.listTools();
        System.out.println("Available GitHub MCP Tools = " + toolsList);

        client.closeGracefully();
    }
}

```

--------------------------------------------------------------------------------
/src/main/java/com/devoxx/agentic/github/GitHubMcpApplication.java:
--------------------------------------------------------------------------------

```java
package com.devoxx.agentic.github;

import com.devoxx.agentic.github.tools.BranchService;
import com.devoxx.agentic.github.tools.CommitService;
import com.devoxx.agentic.github.tools.ContentService;
import com.devoxx.agentic.github.tools.IssueService;
import com.devoxx.agentic.github.tools.PullRequestService;
import com.devoxx.agentic.github.tools.RepositoryService;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class GitHubMcpApplication {

    public static void main(String[] args) {
        SpringApplication.run(GitHubMcpApplication.class, args);
    }

    @Bean
    public ToolCallbackProvider mcpServices(IssueService issueService,
                                            PullRequestService pullRequestService,
                                            RepositoryService repositoryService,
                                            BranchService branchService,
                                            CommitService commitService,
                                            ContentService contentService) {
        return MethodToolCallbackProvider.builder()
                .toolObjects(issueService, pullRequestService, repositoryService, branchService, commitService, contentService)
                .build();
    }
}

```

--------------------------------------------------------------------------------
/src/test/java/com/devoxx/agentic/github/GitHubServiceTest.java:
--------------------------------------------------------------------------------

```java
package com.devoxx.agentic.github;

import com.devoxx.agentic.github.tools.GitHubClientFactory;
import com.devoxx.agentic.github.tools.GitHubEnv;
import io.github.cdimascio.dotenv.Dotenv;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.kohsuke.github.GitHub;
import org.kohsuke.github.GHMyself;

import java.io.IOException;
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.*;

@EnabledIfEnvironmentVariable(named = "GITHUB_TOKEN", matches = ".+")
class GitHubServiceTest {

    @Test
    void testGitHubConnection() throws IOException {
        String githubToken = Dotenv.load().get("GITHUB_TOKEN");
        String githubHost = Dotenv.load().get("GITHUB_HOST");

        System.setProperty("GITHUB_TOKEN", githubToken);
        System.setProperty("GITHUB_HOST", githubHost);

        // Test environment setup
        Optional<GitHubEnv> env = GitHubClientFactory.getEnvironment();
        assertTrue(env.isPresent(), "GitHub environment configuration should be present");
        
        GitHubEnv githubEnv = env.get();
        assertNotNull(githubEnv.githubToken(), "GitHub token should not be null");
        assertNotNull(githubEnv.githubHost(), "GitHub host should not be null");
        
        // Test connection to GitHub API
        GitHub github = GitHubClientFactory.createClient(githubEnv);
        assertNotNull(github, "GitHub client should not be null");
        
        // Test that we can get authenticated user
        GHMyself myself = github.getMyself();
        assertNotNull(myself, "GitHub user should not be null");
        assertNotNull(myself.getLogin(), "GitHub username should not be null");
        
        System.out.println("Successfully connected to GitHub as: " + myself.getLogin());
    }
}

```

--------------------------------------------------------------------------------
/src/main/java/com/devoxx/agentic/github/tools/GitHubClientFactory.java:
--------------------------------------------------------------------------------

```java
package com.devoxx.agentic.github.tools;

import org.kohsuke.github.GitHub;
import java.io.IOException;
import java.util.Optional;

/**
 * Factory for creating GitHub client instances with proper configuration
 */
public class GitHubClientFactory {
    
    /**
     * Creates a GitHub client based on the provided environment configuration
     * 
     * @param env The GitHub environment configuration
     * @return A configured GitHub client
     * @throws IOException if there's an error connecting to GitHub
     */
    public static GitHub createClient(GitHubEnv env) throws IOException {
        if (env.isEnterprise()) {
            return GitHub.connectToEnterprise(env.githubHost(), env.githubToken());
        } else {
            return GitHub.connectUsingOAuth(env.githubToken());
        }
    }

    /**
     * Retrieves environment variables needed for GitHub API access
     * 
     * @return Optional containing GitHub environment, or empty if configuration is missing
     */
    public static Optional<GitHubEnv> getEnvironment() {
        String token = getEnvValue("GITHUB_TOKEN");
        if (token == null || token.isEmpty()) {
            // Try personal access token alternative env var
            token = getEnvValue("GITHUB_PERSONAL_ACCESS_TOKEN");
            if (token == null || token.isEmpty()) {
                System.err.println("WARNING: GitHub token not found in environment variables");
                return Optional.empty();
            }
        }
        
        String host = getEnvValue("GITHUB_HOST");
        if (host == null || host.isEmpty()) {
            // Try GH_HOST alternative
            host = getEnvValue("GH_HOST");
            // Default to github.com if not set
            if (host == null || host.isEmpty()) {
                host = "github.com";
            }
        }
        
        String repository = getEnvValue("GITHUB_REPOSITORY");
        
        // Log information without exposing token
        String tokenPreview = (token.length() > 8) ? 
            token.substring(0, 4) + "..." + token.substring(token.length() - 4) : "****";
        System.out.println("GitHub configuration: Host=" + host + 
                           ", Token=" + tokenPreview + 
                           ", Default repository=" + repository);
        
        return Optional.of(new GitHubEnv(token, host, repository));
    }
    
    /**
     * Helper method to retrieve environment variables from various sources
     */
    private static String getEnvValue(String name) {
        // First check system properties (useful for tests)
        String value = System.getProperty(name);
        if (value != null && !value.isEmpty()) {
            return value;
        }
        
        // Then check environment variables
        return System.getenv(name);
    }
}

```

--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------

```
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.6</version>
        <relativePath /> <!-- lookup parent from repository -->
    </parent>

    <groupId>com.devoxx.agentic</groupId>
    <artifactId>GitHubMCP</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.ai</groupId>
                <artifactId>spring-ai-bom</artifactId>
                <version>1.0.0-SNAPSHOT</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
        </dependency>

        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.15.2</version>
        </dependency>

        <dependency>
            <groupId>io.github.cdimascio</groupId>
            <artifactId>dotenv-java</artifactId>
            <version>3.0.0</version>
        </dependency>
        
        <!-- GitHub API Client -->
        <dependency>
            <groupId>org.kohsuke</groupId>
            <artifactId>github-api</artifactId>
            <version>1.317</version>
        </dependency>

        <!-- Testing dependencies -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <scope>test</scope>
        </dependency>
        
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-junit-jupiter</artifactId>
            <scope>test</scope>
        </dependency>
        
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-core</artifactId>
            <scope>test</scope>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

    <repositories>
        <repository>
            <name>Central Portal Snapshots</name>
            <id>central-portal-snapshots</id>
            <url>https://central.sonatype.com/repository/maven-snapshots/</url>
            <releases>
                <enabled>false</enabled>
            </releases>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </repository>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
        <repository>
            <id>spring-snapshots</id>
            <name>Spring Snapshots</name>
            <url>https://repo.spring.io/snapshot</url>
            <releases>
                <enabled>false</enabled>
            </releases>
        </repository>
    </repositories>
</project>

```

--------------------------------------------------------------------------------
/src/main/java/com/devoxx/agentic/github/tools/RepositoryService.java:
--------------------------------------------------------------------------------

```java
package com.devoxx.agentic.github.tools;

import org.kohsuke.github.*;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.*;

/**
 * Service for GitHub repository-related operations
 */
@Service
public class RepositoryService extends AbstractToolService {

    @Tool(description = """
        List repositories for the authenticated user.
        Returns a list of repositories the user has access to.
    """)
    public String listRepositories(
            @ToolParam(description = "Maximum number of repositories to return", required = false) Integer limit
    ) {
        Map<String, Object> result = new HashMap<>();
        
        try {
            Optional<GitHubEnv> env = GitHubClientFactory.getEnvironment();
            if (env.isEmpty()) {
                return errorMessage("GitHub is not configured correctly");
            }
            
            GitHubEnv githubEnv = env.get();
            GitHub github = GitHubClientFactory.createClient(githubEnv);
            
            GHMyself myself = github.getMyself();
            Map<String, GHRepository> repos = myself.getAllRepositories();
            
            List<Map<String, Object>> repoList = new ArrayList<>();
            int count = 0;
            
            for (GHRepository repo : repos.values()) {
                if (limit != null && count >= limit) {
                    break;
                }
                
                Map<String, Object> repoData = new HashMap<>();
                repoData.put("name", repo.getName());
                repoData.put("full_name", repo.getFullName());
                repoData.put("description", repo.getDescription());
                repoData.put("url", repo.getHtmlUrl().toString());
                repoData.put("stars", repo.getStargazersCount());
                repoData.put("forks", repo.listForks().toList().size());
                repoData.put("private", repo.isPrivate());
                
                repoList.add(repoData);
                count++;
            }
            
            result.put("repositories", repoList);
            result.put("total_count", repos.size());
            
            return successMessage(result);
            
        } catch (IOException e) {
            return errorMessage("IO error: " + e.getMessage());
        } catch (Exception e) {
            return errorMessage("Unexpected error: " + e.getMessage());
        }
    }

    @Tool(description = """
        Get information about a specific repository.
        Returns details about the repository such as description, stars, forks, etc.
    """)
    public String getRepository(
            @ToolParam(description = "Repository name in format 'owner/repo'", required = false) String repository
    ) {
        Map<String, Object> result = new HashMap<>();
        
        try {
            Optional<GitHubEnv> env = GitHubClientFactory.getEnvironment();
            if (env.isEmpty()) {
                return errorMessage("GitHub is not configured correctly");
            }
            
            GitHubEnv githubEnv = env.get();
            GitHub github = GitHubClientFactory.createClient(githubEnv);
            
            // Use provided repository or default from environment
            String repoName = (repository != null && !repository.isEmpty()) ? 
                                repository : githubEnv.getFullRepository();
                                
            if (repoName == null || repoName.isEmpty()) {
                return errorMessage("Repository name is required");
            }
            
            GHRepository repo = github.getRepository(repoName);
            
            Map<String, Object> repoData = new HashMap<>();
            repoData.put("name", repo.getName());
            repoData.put("full_name", repo.getFullName());
            repoData.put("description", repo.getDescription());
            repoData.put("url", repo.getHtmlUrl().toString());
            repoData.put("stars", repo.getStargazersCount());
            repoData.put("forks", repo.listForks().toList().size());
            repoData.put("open_issues", repo.getOpenIssueCount());
            repoData.put("watchers", repo.getWatchersCount());
            repoData.put("license", repo.getLicense() != null ? repo.getLicense().getName() : null);
            repoData.put("default_branch", repo.getDefaultBranch());
            repoData.put("created_at", repo.getCreatedAt().toString());
            repoData.put("updated_at", repo.getUpdatedAt().toString());
            repoData.put("private", repo.isPrivate());
            
            result.put("repository", repoData);
            
            return successMessage(result);
            
        } catch (GHFileNotFoundException e) {
            return errorMessage("Repository not found: " + e.getMessage());
        } catch (IOException e) {
            return errorMessage("IO error: " + e.getMessage());
        } catch (Exception e) {
            return errorMessage("Unexpected error: " + e.getMessage());
        }
    }
    
    @Tool(description = """
        Search for repositories.
        Searches GitHub for repositories matching the query.
    """)
    public String searchRepositories(
            @ToolParam(description = "Search query") String query,
            @ToolParam(description = "Maximum number of results to return", required = false) Integer limit
    ) {
        Map<String, Object> result = new HashMap<>();
        
        try {
            if (query == null || query.isEmpty()) {
                return errorMessage("Search query is required");
            }
            
            Optional<GitHubEnv> env = GitHubClientFactory.getEnvironment();
            if (env.isEmpty()) {
                return errorMessage("GitHub is not configured correctly");
            }
            
            GitHubEnv githubEnv = env.get();
            GitHub github = GitHubClientFactory.createClient(githubEnv);
            
            int actualLimit = (limit != null && limit > 0) ? limit : 10;
            
            GHRepositorySearchBuilder searchBuilder = github.searchRepositories()
                    .q(query)
                    .order(GHDirection.DESC)
                    .sort(GHRepositorySearchBuilder.Sort.STARS);
            
            List<Map<String, Object>> repoList = new ArrayList<>();
            
            searchBuilder.list().withPageSize(actualLimit).iterator().forEachRemaining(repo -> {
                if (repoList.size() < actualLimit) {
                    Map<String, Object> repoData = new HashMap<>();
                    repoData.put("name", repo.getName());
                    repoData.put("full_name", repo.getFullName());
                    repoData.put("description", repo.getDescription());
                    repoData.put("url", repo.getHtmlUrl().toString());
                    repoData.put("stars", repo.getStargazersCount());
                    try {
                        repoData.put("forks", repo.listForks().toList().size());
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                    repoData.put("language", repo.getLanguage());
                    
                    repoList.add(repoData);
                }
            });
            
            result.put("repositories", repoList);
            result.put("query", query);
            
            return successMessage(result);
            
        } catch (IOException e) {
            return errorMessage("IO error: " + e.getMessage());
        } catch (Exception e) {
            return errorMessage("Unexpected error: " + e.getMessage());
        }
    }
}

```

--------------------------------------------------------------------------------
/src/main/java/com/devoxx/agentic/github/tools/BranchService.java:
--------------------------------------------------------------------------------

```java
package com.devoxx.agentic.github.tools;

import org.kohsuke.github.*;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.*;

/**
 * Service for GitHub branch-related operations
 */
@Service
public class BranchService extends AbstractToolService {

    @Tool(description = """
    List branches in a repository.
    Returns a list of branches with their details.
    """)
    public String listBranches(
            @ToolParam(description = "Repository name in format 'owner/repo'", required = false) String repository,
            @ToolParam(description = "Filter branches by prefix", required = false) String filter,
            @ToolParam(description = "Maximum number of branches to return", required = false) Integer limit
    ) {
        Map<String, Object> result = new HashMap<>();
        
        try {
            Optional<GitHubEnv> env = GitHubClientFactory.getEnvironment();
            if (env.isEmpty()) {
                return errorMessage("GitHub is not configured correctly");
            }
            
            GitHubEnv githubEnv = env.get();
            GitHub github = GitHubClientFactory.createClient(githubEnv);
            
            // Use provided repository or default from environment
            String repoName = (repository != null && !repository.isEmpty()) ? 
                                repository : githubEnv.getFullRepository();
                                
            if (repoName == null || repoName.isEmpty()) {
                return errorMessage("Repository name is required");
            }
            
            GHRepository repo = github.getRepository(repoName);
            
            List<Map<String, Object>> branchList = new ArrayList<>();
            int count = 0;
            
            for (GHBranch branch : repo.getBranches().values()) {
                // Apply filter if provided
                if (filter != null && !filter.isEmpty() && !branch.getName().startsWith(filter)) {
                    continue;
                }
                
                if (limit != null && count >= limit) {
                    break;
                }
                
                Map<String, Object> branchData = new HashMap<>();
                branchData.put("name", branch.getName());
                branchData.put("sha", branch.getSHA1());
                
                // Get the latest commit for this branch
                GHCommit commit = repo.getCommit(branch.getSHA1());
                if (commit != null) {
                    Map<String, Object> commitData = new HashMap<>();
                    commitData.put("message", commit.getCommitShortInfo().getMessage());
                    commitData.put("author", commit.getCommitShortInfo().getAuthor().getName());
                    commitData.put("date", commit.getCommitShortInfo().getCommitDate().toString());
                    branchData.put("latest_commit", commitData);
                }
                
                // Check if this is the default branch
                branchData.put("is_default", branch.getName().equals(repo.getDefaultBranch()));
                
                // Get protection status (requires separate API call)
                try {
                    GHBranchProtection protection = repo.getBranch(branch.getName()).getProtection();
                    branchData.put("protected", true);
                    // Add protection details if needed
                } catch (GHFileNotFoundException ex) {
                    // Branch is not protected
                    branchData.put("protected", false);
                } catch (Exception ex) {
                    // Ignore other exceptions for protection status
                    branchData.put("protected", false);
                }
                
                branchList.add(branchData);
                count++;
            }
            
            result.put("branches", branchList);
            result.put("total_count", branchList.size());
            
            return successMessage(result);
            
        } catch (GHFileNotFoundException e) {
            return errorMessage("Repository not found: " + e.getMessage());
        } catch (IOException e) {
            return errorMessage("IO error: " + e.getMessage());
        } catch (Exception e) {
            return errorMessage("Unexpected error: " + e.getMessage());
        }
    }

    @Tool(description = """
    Create a new branch in a repository.
    Creates a branch from a specified SHA or reference (defaults to the default branch if not specified).
    """)
    public String createBranch(
            @ToolParam(description = "Repository name in format 'owner/repo'", required = false) String repository,
            @ToolParam(description = "New branch name") String branchName,
            @ToolParam(description = "SHA or reference to create branch from (defaults to default branch)", required = false) String fromRef
    ) {
        Map<String, Object> result = new HashMap<>();
        
        try {
            if (branchName == null || branchName.isEmpty()) {
                return errorMessage("Branch name is required");
            }
            
            Optional<GitHubEnv> env = GitHubClientFactory.getEnvironment();
            if (env.isEmpty()) {
                return errorMessage("GitHub is not configured correctly");
            }
            
            GitHubEnv githubEnv = env.get();
            GitHub github = GitHubClientFactory.createClient(githubEnv);
            
            // Use provided repository or default from environment
            String repoName = (repository != null && !repository.isEmpty()) ? 
                                repository : githubEnv.getFullRepository();
                                
            if (repoName == null || repoName.isEmpty()) {
                return errorMessage("Repository name is required");
            }
            
            GHRepository repo = github.getRepository(repoName);
            
            // Check if branch already exists
            try {
                GHBranch existingBranch = repo.getBranch(branchName);
                if (existingBranch != null) {
                    return errorMessage("Branch '" + branchName + "' already exists");
                }
            } catch (GHFileNotFoundException ex) {
                // Branch doesn't exist, which is what we want
            }
            
            // Determine the SHA to create from
            String sha;
            if (fromRef != null && !fromRef.isEmpty()) {
                try {
                    // Try to get the SHA from the reference
                    GHRef ref = repo.getRef("heads/" + fromRef);
                    sha = ref.getObject().getSha();
                } catch (GHFileNotFoundException ex) {
                    try {
                        // Try to resolve as a direct SHA
                        GHCommit commit = repo.getCommit(fromRef);
                        sha = commit.getSHA1();
                    } catch (Exception e) {
                        return errorMessage("Invalid reference: " + fromRef);
                    }
                }
            } else {
                // Use default branch
                String defaultBranch = repo.getDefaultBranch();
                GHRef ref = repo.getRef("heads/" + defaultBranch);
                sha = ref.getObject().getSha();
            }
            
            // Create the new branch
            GHRef newBranch = repo.createRef("refs/heads/" + branchName, sha);
            
            Map<String, Object> branchData = new HashMap<>();
            branchData.put("name", branchName);
            branchData.put("sha", sha);
            branchData.put("url", newBranch.getUrl().toString());
            
            result.put("branch", branchData);
            
            return successMessage(result);
            
        } catch (GHFileNotFoundException e) {
            return errorMessage("Repository not found: " + e.getMessage());
        } catch (IOException e) {
            return errorMessage("IO error: " + e.getMessage());
        } catch (Exception e) {
            return errorMessage("Unexpected error: " + e.getMessage());
        }
    }
}

```

--------------------------------------------------------------------------------
/src/main/java/com/devoxx/agentic/github/tools/CommitService.java:
--------------------------------------------------------------------------------

```java
package com.devoxx.agentic.github.tools;

import org.kohsuke.github.*;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.*;

/**
 * Service for GitHub commit-related operations
 */
@Service
public class CommitService extends AbstractToolService {

    @Tool(description = """
        Get detailed information about a specific commit.
        Returns commit data including author, committer, message, and file changes.
    """)
    public String getCommitDetails(
            @ToolParam(description = "Repository name in format 'owner/repo'", required = false) String repository,
            @ToolParam(description = "Commit SHA") String sha
    ) {
        Map<String, Object> result = new HashMap<>();
        
        try {
            if (sha == null || sha.isEmpty()) {
                return errorMessage("Commit SHA is required");
            }
            
            Optional<GitHubEnv> env = GitHubClientFactory.getEnvironment();
            if (env.isEmpty()) {
                return errorMessage("GitHub is not configured correctly");
            }
            
            GitHubEnv githubEnv = env.get();
            GitHub github = GitHubClientFactory.createClient(githubEnv);
            
            // Use provided repository or default from environment
            String repoName = (repository != null && !repository.isEmpty()) ? 
                                repository : githubEnv.getFullRepository();
                                
            if (repoName == null || repoName.isEmpty()) {
                return errorMessage("Repository name is required");
            }
            
            GHRepository repo = github.getRepository(repoName);
            GHCommit commit = repo.getCommit(sha);
            
            Map<String, Object> commitData = new HashMap<>();
            commitData.put("sha", commit.getSHA1());
            commitData.put("message", commit.getCommitShortInfo().getMessage());
            commitData.put("html_url", commit.getHtmlUrl().toString());
            
            // Author details
            GHCommit.ShortInfo info = commit.getCommitShortInfo();
            Map<String, Object> authorData = new HashMap<>();
            authorData.put("name", info.getAuthor().getName());
            authorData.put("email", info.getAuthor().getEmail());
            authorData.put("date", info.getAuthoredDate().toString());
            commitData.put("author", authorData);
            
            // Committer details (might be different from author)
            Map<String, Object> committerData = new HashMap<>();
            committerData.put("name", info.getCommitter().getName());
            committerData.put("email", info.getCommitter().getEmail());
            committerData.put("date", info.getCommitDate().toString());
            commitData.put("committer", committerData);
            
            // Parents
            List<Map<String, String>> parentsList = new ArrayList<>();
            for (GHCommit parent : commit.getParents()) {
                Map<String, String> parentData = new HashMap<>();
                parentData.put("sha", parent.getSHA1());
                parentData.put("url", parent.getHtmlUrl().toString());
                parentsList.add(parentData);
            }
            commitData.put("parents", parentsList);
            
            // File changes
            List<Map<String, Object>> filesList = new ArrayList<>();
            for (GHCommit.File file : commit.listFiles()) {
                Map<String, Object> fileData = new HashMap<>();
                fileData.put("filename", file.getFileName());
                fileData.put("status", file.getStatus());
                fileData.put("additions", file.getLinesAdded());
                fileData.put("deletions", file.getLinesDeleted());
                fileData.put("changes", file.getLinesChanged());
                fileData.put("patch", file.getPatch());
                filesList.add(fileData);
            }
            commitData.put("files", filesList);
            
            // Stats
            Map<String, Integer> statsData = new HashMap<>();
            statsData.put("additions", commit.getLinesAdded());
            statsData.put("deletions", commit.getLinesDeleted());
            statsData.put("total", commit.getLinesChanged());
            commitData.put("stats", statsData);
            
            result.put("commit", commitData);
            
            return successMessage(result);
            
        } catch (GHFileNotFoundException e) {
            return errorMessage("Commit or repository not found: " + e.getMessage());
        } catch (IOException e) {
            return errorMessage("IO error: " + e.getMessage());
        } catch (Exception e) {
            return errorMessage("Unexpected error: " + e.getMessage());
        }
    }

    @Tool(description = """
    List commits in a repository.
    Returns a list of commits with filtering options for branch and author.
    """)
    public String listCommits(
            @ToolParam(description = "Repository name in format 'owner/repo'", required = false) String repository,
            @ToolParam(description = "Branch or tag name to filter commits", required = false) String branch,
            @ToolParam(description = "Author name or email to filter commits", required = false) String author,
            @ToolParam(description = "Path to filter commits that touch the specified path", required = false) String path,
            @ToolParam(description = "Maximum number of commits to return", required = false) Integer limit
    ) {
        Map<String, Object> result = new HashMap<>();
        
        try {
            Optional<GitHubEnv> env = GitHubClientFactory.getEnvironment();
            if (env.isEmpty()) {
                return errorMessage("GitHub is not configured correctly");
            }
            
            GitHubEnv githubEnv = env.get();
            GitHub github = GitHubClientFactory.createClient(githubEnv);
            
            // Use provided repository or default from environment
            String repoName = (repository != null && !repository.isEmpty()) ? 
                                repository : githubEnv.getFullRepository();
                                
            if (repoName == null || repoName.isEmpty()) {
                return errorMessage("Repository name is required");
            }
            
            GHRepository repo = github.getRepository(repoName);
            
            // Setup commit query parameters
            GHCommitQueryBuilder queryBuilder = repo.queryCommits();
            
            if (branch != null && !branch.isEmpty()) {
                queryBuilder.from(branch);
            }
            
            if (author != null && !author.isEmpty()) {
                queryBuilder.author(author);
            }
            
            if (path != null && !path.isEmpty()) {
                queryBuilder.path(path);
            }
            
            int actualLimit = (limit != null && limit > 0) ? limit : 30; // Default to 30 commits
            
            List<Map<String, Object>> commitList = new ArrayList<>();
            int count = 0;
            
            for (GHCommit commit : queryBuilder.list().withPageSize(actualLimit)) {
                if (count >= actualLimit) {
                    break;
                }
                
                Map<String, Object> commitData = new HashMap<>();
                commitData.put("sha", commit.getSHA1());
                
                GHCommit.ShortInfo info = commit.getCommitShortInfo();
                commitData.put("message", info.getMessage());
                commitData.put("author", info.getAuthor().getName());
                commitData.put("author_email", info.getAuthor().getEmail());
                commitData.put("date", info.getAuthoredDate().toString());
                
                // Include stats
                Map<String, Integer> statsData = new HashMap<>();
                statsData.put("additions", commit.getLinesAdded());
                statsData.put("deletions", commit.getLinesDeleted());
                statsData.put("total", commit.getLinesChanged());
                commitData.put("stats", statsData);
                
                commitData.put("html_url", commit.getHtmlUrl().toString());
                
                commitList.add(commitData);
                count++;
            }
            
            result.put("commits", commitList);
            result.put("count", commitList.size());
            
            if (branch != null && !branch.isEmpty()) {
                result.put("branch", branch);
            }
            
            if (author != null && !author.isEmpty()) {
                result.put("author", author);
            }
            
            if (path != null && !path.isEmpty()) {
                result.put("path", path);
            }
            
            return successMessage(result);
            
        } catch (GHFileNotFoundException e) {
            return errorMessage("Repository not found: " + e.getMessage());
        } catch (IOException e) {
            return errorMessage("IO error: " + e.getMessage());
        } catch (Exception e) {
            return errorMessage("Unexpected error: " + e.getMessage());
        }
    }

    @Tool(description = """
    Search for a commit based on the provided text or keywords in the project history.
    Useful for finding specific change sets or code modifications by commit messages or diff content.
    Takes a query parameter and returns the matching commit information.
    Returns matched commit hashes as a JSON array.
    """)
    public String findCommitByMessage(
            @ToolParam(description = "Text to search for in commit messages") String text,
            @ToolParam(description = "Repository name in format 'owner/repo'", required = false) String repository,
            @ToolParam(description = "Branch or tag name to search within", required = false) String branch,
            @ToolParam(description = "Maximum number of results to return", required = false) Integer limit
    ) {
        Map<String, Object> result = new HashMap<>();
        
        try {
            if (text == null || text.isEmpty()) {
                return errorMessage("Search text is required");
            }
            
            Optional<GitHubEnv> env = GitHubClientFactory.getEnvironment();
            if (env.isEmpty()) {
                return errorMessage("GitHub is not configured correctly");
            }
            
            GitHubEnv githubEnv = env.get();
            GitHub github = GitHubClientFactory.createClient(githubEnv);
            
            // Use provided repository or default from environment
            String repoName = (repository != null && !repository.isEmpty()) ? 
                                repository : githubEnv.getFullRepository();
                                
            if (repoName == null || repoName.isEmpty()) {
                return errorMessage("Repository name is required");
            }
            
            GHRepository repo = github.getRepository(repoName);
            
            // Setup query parameters
            GHCommitQueryBuilder queryBuilder = repo.queryCommits();
            
            if (branch != null && !branch.isEmpty()) {
                queryBuilder.from(branch);
            }
            
            // Normalize the search text to lowercase for case-insensitive matching
            String searchText = text.toLowerCase();
            int actualLimit = (limit != null && limit > 0) ? limit : 20; // Default to 20 results
            
            List<Map<String, Object>> matchedCommits = new ArrayList<>();
            int count = 0;
            
            for (GHCommit commit : queryBuilder.list()) {
                if (count >= actualLimit) {
                    break;
                }
                
                GHCommit.ShortInfo info = commit.getCommitShortInfo();
                String message = info.getMessage();
                
                // Check if the commit message contains the search text
                if (message != null && message.toLowerCase().contains(searchText)) {
                    Map<String, Object> commitData = new HashMap<>();
                    commitData.put("sha", commit.getSHA1());
                    commitData.put("message", message);
                    commitData.put("author", info.getAuthor().getName());
                    commitData.put("date", info.getAuthoredDate().toString());
                    commitData.put("html_url", commit.getHtmlUrl().toString());
                    
                    // Include stats
                    Map<String, Integer> statsData = new HashMap<>();
                    statsData.put("additions", commit.getLinesAdded());
                    statsData.put("deletions", commit.getLinesDeleted());
                    statsData.put("total", commit.getLinesChanged());
                    commitData.put("stats", statsData);
                    
                    matchedCommits.add(commitData);
                    count++;
                }
            }
            
            result.put("commits", matchedCommits);
            result.put("count", matchedCommits.size());
            result.put("search_text", text);
            
            if (branch != null && !branch.isEmpty()) {
                result.put("branch", branch);
            }
            
            return successMessage(result);
            
        } catch (GHFileNotFoundException e) {
            return errorMessage("Repository not found: " + e.getMessage());
        } catch (IOException e) {
            return errorMessage("IO error: " + e.getMessage());
        } catch (Exception e) {
            return errorMessage("Unexpected error: " + e.getMessage());
        }
    }
}

```

--------------------------------------------------------------------------------
/src/main/java/com/devoxx/agentic/github/tools/PullRequestService.java:
--------------------------------------------------------------------------------

```java
package com.devoxx.agentic.github.tools;

import org.kohsuke.github.*;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.*;

/**
 * Service for GitHub pull request-related operations
 */
@Service
public class PullRequestService extends AbstractToolService {

    @Tool(description = """
    List pull requests for a repository.
    Returns pull requests with filtering options for state.
    """)
    public String listPullRequests(
            @ToolParam(description = "Repository name in format 'owner/repo'", required = false) String repository,
            @ToolParam(description = "State of pull requests (open, closed, all)", required = false) String state,
            @ToolParam(description = "Maximum number of results to return", required = false) Integer limit
    ) {
        Map<String, Object> result = new HashMap<>();
        
        try {
            Optional<GitHubEnv> env = GitHubClientFactory.getEnvironment();
            if (env.isEmpty()) {
                return errorMessage("GitHub is not configured correctly");
            }
            
            GitHubEnv githubEnv = env.get();
            GitHub github = GitHubClientFactory.createClient(githubEnv);
            
            // Use provided repository or default from environment
            String repoName = (repository != null && !repository.isEmpty()) ? 
                                repository : githubEnv.getFullRepository();
                                
            if (repoName == null || repoName.isEmpty()) {
                return errorMessage("Repository name is required");
            }
            
            GHRepository repo = github.getRepository(repoName);
            
            GHIssueState prState = GHIssueState.OPEN;
            if (state != null) {
                prState = switch (state.toLowerCase()) {
                    case "closed" -> GHIssueState.CLOSED;
                    case "all" -> GHIssueState.ALL;
                    default -> prState;
                };
            }
            
            List<GHPullRequest> pullRequests = repo.getPullRequests(prState);
            List<Map<String, Object>> prList = new ArrayList<>();
            int count = 0;
            
            for (GHPullRequest pr : pullRequests) {
                if (limit != null && count >= limit) {
                    break;
                }
                
                Map<String, Object> prData = new HashMap<>();
                prData.put("number", pr.getNumber());
                prData.put("title", pr.getTitle());
                prData.put("state", pr.getState().name().toLowerCase());
                prData.put("html_url", pr.getHtmlUrl().toString());
                prData.put("created_at", pr.getCreatedAt().toString());
                prData.put("updated_at", pr.getUpdatedAt().toString());
                prData.put("closed_at", pr.getClosedAt() != null ? pr.getClosedAt().toString() : null);
                prData.put("merged_at", pr.getMergedAt() != null ? pr.getMergedAt().toString() : null);
                prData.put("is_merged", pr.isMerged());
                
                prData.put("user", pr.getUser().getLogin());
                
                prData.put("base_branch", pr.getBase().getRef());
                prData.put("head_branch", pr.getHead().getRef());
                
                prList.add(prData);
                count++;
            }
            
            result.put("pull_requests", prList);
            result.put("total_count", pullRequests.size());
            
            return successMessage(result);
            
        } catch (GHFileNotFoundException e) {
            return errorMessage("Repository not found: " + e.getMessage());
        } catch (IOException e) {
            return errorMessage("IO error: " + e.getMessage());
        } catch (Exception e) {
            return errorMessage("Unexpected error: " + e.getMessage());
        }
    }

    @Tool(description = """
    Get a specific pull request.
    Returns detailed information about the pull request.
    """)
    public String getPullRequest(
            @ToolParam(description = "Repository name in format 'owner/repo'", required = false) String repository,
            @ToolParam(description = "Pull request number") Integer prNumber
    ) {
        Map<String, Object> result = new HashMap<>();
        
        try {
            if (prNumber == null) {
                return errorMessage("Pull request number is required");
            }
            
            Optional<GitHubEnv> env = GitHubClientFactory.getEnvironment();
            if (env.isEmpty()) {
                return errorMessage("GitHub is not configured correctly");
            }
            
            GitHubEnv githubEnv = env.get();
            GitHub github = GitHubClientFactory.createClient(githubEnv);
            
            // Use provided repository or default from environment
            String repoName = (repository != null && !repository.isEmpty()) ? 
                                repository : githubEnv.getFullRepository();
                                
            if (repoName == null || repoName.isEmpty()) {
                return errorMessage("Repository name is required");
            }
            
            GHRepository repo = github.getRepository(repoName);
            GHPullRequest pr = repo.getPullRequest(prNumber);
            
            Map<String, Object> prData = new HashMap<>();
            prData.put("number", pr.getNumber());
            prData.put("title", pr.getTitle());
            prData.put("body", pr.getBody());
            prData.put("state", pr.getState().name().toLowerCase());
            prData.put("html_url", pr.getHtmlUrl().toString());
            prData.put("created_at", pr.getCreatedAt().toString());
            prData.put("updated_at", pr.getUpdatedAt().toString());
            prData.put("closed_at", pr.getClosedAt() != null ? pr.getClosedAt().toString() : null);
            prData.put("merged_at", pr.getMergedAt() != null ? pr.getMergedAt().toString() : null);
            prData.put("is_merged", pr.isMerged());
            
            prData.put("user", pr.getUser().getLogin());
            
            prData.put("base_branch", pr.getBase().getRef());
            prData.put("head_branch", pr.getHead().getRef());
            
            // Get comments
            List<Map<String, Object>> commentsList = new ArrayList<>();
            for (GHIssueComment comment : pr.getComments()) {
                Map<String, Object> commentData = new HashMap<>();
                commentData.put("id", comment.getId());
                commentData.put("user", comment.getUser().getLogin());
                commentData.put("body", comment.getBody());
                commentData.put("created_at", comment.getCreatedAt().toString());
                commentData.put("updated_at", comment.getUpdatedAt().toString());
                
                commentsList.add(commentData);
            }
            
            prData.put("comments", commentsList);
            
            // Get files
            List<Map<String, Object>> filesList = new ArrayList<>();
            for (GHPullRequestFileDetail file : pr.listFiles()) {
                Map<String, Object> fileData = new HashMap<>();
                fileData.put("filename", file.getFilename());
                fileData.put("status", file.getStatus());
                fileData.put("additions", file.getAdditions());
                fileData.put("deletions", file.getDeletions());
                fileData.put("changes", file.getChanges());
                
                filesList.add(fileData);
            }
            
            prData.put("files", filesList);
            
            result.put("pull_request", prData);
            
            return successMessage(result);
            
        } catch (GHFileNotFoundException e) {
            return errorMessage("Pull request or repository not found: " + e.getMessage());
        } catch (IOException e) {
            return errorMessage("IO error: " + e.getMessage());
        } catch (Exception e) {
            return errorMessage("Unexpected error: " + e.getMessage());
        }
    }
    
    @Tool(description = """
        Create a comment on a pull request.
        Posts a new comment on the specified pull request.
    """)
    public String createPullRequestComment(
            @ToolParam(description = "Repository name in format 'owner/repo'", required = false) String repository,
            @ToolParam(description = "Pull request number") Integer prNumber,
            @ToolParam(description = "Comment text") String body
    ) {
        Map<String, Object> result = new HashMap<>();
        
        try {
            if (prNumber == null) {
                return errorMessage("Pull request number is required");
            }
            
            if (body == null || body.isEmpty()) {
                return errorMessage("Comment body is required");
            }
            
            Optional<GitHubEnv> env = GitHubClientFactory.getEnvironment();
            if (env.isEmpty()) {
                return errorMessage("GitHub is not configured correctly");
            }
            
            GitHubEnv githubEnv = env.get();
            GitHub github = GitHubClientFactory.createClient(githubEnv);
            
            // Use provided repository or default from environment
            String repoName = (repository != null && !repository.isEmpty()) ? 
                                repository : githubEnv.getFullRepository();
                                
            if (repoName == null || repoName.isEmpty()) {
                return errorMessage("Repository name is required");
            }
            
            GHRepository repo = github.getRepository(repoName);
            GHPullRequest pr = repo.getPullRequest(prNumber);
            
            GHIssueComment comment = pr.comment(body);
            
            Map<String, Object> commentData = new HashMap<>();
            commentData.put("id", comment.getId());
            commentData.put("body", comment.getBody());
            commentData.put("html_url", comment.getHtmlUrl().toString());
            commentData.put("created_at", comment.getCreatedAt().toString());
            
            result.put("comment", commentData);
            
            return successMessage(result);
            
        } catch (GHFileNotFoundException e) {
            return errorMessage("Pull request or repository not found: " + e.getMessage());
        } catch (IOException e) {
            return errorMessage("IO error: " + e.getMessage());
        } catch (Exception e) {
            return errorMessage("Unexpected error: " + e.getMessage());
        }
    }
    
    @Tool(description = """
        Merge a pull request.
        Merges the pull request with the specified merge method.
    """)
    public String mergePullRequest(
            @ToolParam(description = "Repository name in format 'owner/repo'", required = false) String repository,
            @ToolParam(description = "Pull request number") Integer prNumber,
            @ToolParam(description = "Commit message for the merge", required = false) String commitMessage,
            @ToolParam(description = "Merge method (merge, squash, rebase)", required = false) String mergeMethod
    ) {
        Map<String, Object> result = new HashMap<>();
        
        try {
            if (prNumber == null) {
                return errorMessage("Pull request number is required");
            }
            
            Optional<GitHubEnv> env = GitHubClientFactory.getEnvironment();
            if (env.isEmpty()) {
                return errorMessage("GitHub is not configured correctly");
            }
            
            GitHubEnv githubEnv = env.get();
            GitHub github = GitHubClientFactory.createClient(githubEnv);
            
            // Use provided repository or default from environment
            String repoName = (repository != null && !repository.isEmpty()) ? 
                                repository : githubEnv.getFullRepository();
                                
            if (repoName == null || repoName.isEmpty()) {
                return errorMessage("Repository name is required");
            }
            
            GHRepository repo = github.getRepository(repoName);
            GHPullRequest pr = repo.getPullRequest(prNumber);
            
            // Check if PR is already merged
            if (pr.isMerged()) {
                return errorMessage("Pull request is already merged");
            }
            
            // Set default merge method if not provided
            String method = (mergeMethod != null && !mergeMethod.isEmpty()) ? 
                             mergeMethod.toLowerCase() : "merge";
            
            boolean success = switch (method) {
                case "squash" -> {
                    pr.merge(commitMessage, null, GHPullRequest.MergeMethod.SQUASH);
                    yield true;
                }
                case "rebase" -> {
                    pr.merge(commitMessage, null, GHPullRequest.MergeMethod.REBASE);
                    yield true;
                }
                case "merge" -> {
                    pr.merge(commitMessage, null, GHPullRequest.MergeMethod.MERGE);
                    yield true;
                }
                default -> false;
            };

            if (success) {
                result.put("merged", true);
                result.put("method", method);
                result.put("pull_request_number", prNumber);
                result.put("repository", repoName);

                return successMessage(result);
            } else {
                return errorMessage("Failed to merge pull request");
            }

        } catch (GHFileNotFoundException e) {
            return errorMessage("Pull request or repository not found: " + e.getMessage());
        } catch (IOException e) {
            return errorMessage("IO error: " + e.getMessage());
        } catch (Exception e) {
            return errorMessage("Unexpected error: " + e.getMessage());
        }
    }
}

```

--------------------------------------------------------------------------------
/src/main/java/com/devoxx/agentic/github/tools/ContentService.java:
--------------------------------------------------------------------------------

```java
package com.devoxx.agentic.github.tools;

import org.kohsuke.github.*;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.*;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

/**
 * Service for GitHub repository content management operations
 */
@Service
public class ContentService extends AbstractToolService {

    @Tool(description = """
        Get the contents of a file in a repository.
        Returns the file content and metadata such as size and sha.
    """)
    public String getFileContents(
            @ToolParam(description = "Repository name in format 'owner/repo'", required = false) String repository,
            @ToolParam(description = "Path to the file in the repository") String path,
            @ToolParam(description = "Branch or commit SHA (defaults to the default branch)", required = false) String ref
    ) {
        Map<String, Object> result = new HashMap<>();
        
        try {
            if (path == null || path.isEmpty()) {
                return errorMessage("File path is required");
            }
            
            Optional<GitHubEnv> env = GitHubClientFactory.getEnvironment();
            if (env.isEmpty()) {
                return errorMessage("GitHub is not configured correctly");
            }
            
            GitHubEnv githubEnv = env.get();
            GitHub github = GitHubClientFactory.createClient(githubEnv);
            
            // Use provided repository or default from environment
            String repoName = (repository != null && !repository.isEmpty()) ? 
                                repository : githubEnv.getFullRepository();
                                
            if (repoName == null || repoName.isEmpty()) {
                return errorMessage("Repository name is required");
            }
            
            GHRepository repo = github.getRepository(repoName);
            
            // Get contents, using ref if provided
            GHContent content;
            if (ref != null && !ref.isEmpty()) {
                content = repo.getFileContent(path, ref);
            } else {
                content = repo.getFileContent(path);
            }
            
            // Check if it's a file
            if (content.isDirectory()) {
                return errorMessage("Path points to a directory, not a file");
            }

            var contentData = getContentDetails(content);
            contentData.put("type", content.getType());
            contentData.put("url", content.getHtmlUrl());
            contentData.put("download_url", content.getDownloadUrl());
            
            // Get and decode content 
            String base64Content = content.getContent();
            if (base64Content != null) {
                // The content is base64 encoded
                String decodedContent = new String(Base64.getDecoder().decode(base64Content), StandardCharsets.UTF_8);
                contentData.put("content", decodedContent);
            } else {
                contentData.put("content", "");
            }
            
            result.put("file", contentData);
            
            return successMessage(result);
            
        } catch (GHFileNotFoundException e) {
            return errorMessage("File or repository not found: " + e.getMessage());
        } catch (IOException e) {
            return errorMessage("IO error: " + e.getMessage());
        } catch (Exception e) {
            return errorMessage("Unexpected error: " + e.getMessage());
        }
    }

    @Tool(description = """
    List contents of a directory in a repository.
    Returns a list of files and directories at the specified path.
    """)
    public String listDirectoryContents(
            @ToolParam(description = "Repository name in format 'owner/repo'", required = false) String repository,
            @ToolParam(description = "Path to the directory in the repository (use '/' for root)", required = false) String path,
            @ToolParam(description = "Branch or commit SHA (defaults to the default branch)", required = false) String ref
    ) {
        Map<String, Object> result = new HashMap<>();
        
        try {
            Optional<GitHubEnv> env = GitHubClientFactory.getEnvironment();
            if (env.isEmpty()) {
                return errorMessage("GitHub is not configured correctly");
            }
            
            GitHubEnv githubEnv = env.get();
            GitHub github = GitHubClientFactory.createClient(githubEnv);
            
            // Use provided repository or default from environment
            String repoName = (repository != null && !repository.isEmpty()) ? 
                                repository : githubEnv.getFullRepository();
                                
            if (repoName == null || repoName.isEmpty()) {
                return errorMessage("Repository name is required");
            }
            
            GHRepository repo = github.getRepository(repoName);
            
            // Set default path if not provided
            String dirPath = (path != null && !path.isEmpty()) ? path : "";
            
            // Get contents, using ref if provided
            List<GHContent> contents;
            if (ref != null && !ref.isEmpty()) {
                contents = repo.getDirectoryContent(dirPath, ref);
            } else {
                contents = repo.getDirectoryContent(dirPath);
            }
            
            List<Map<String, Object>> contentsList = new ArrayList<>();
            
            for (GHContent content : contents) {
                Map<String, Object> contentData = getContentDetails(content);
                contentData.put("type", content.isDirectory() ? "directory" : "file");
                contentData.put("url", content.getHtmlUrl());
                if (!content.isDirectory()) {
                    contentData.put("download_url", content.getDownloadUrl());
                }
                
                contentsList.add(contentData);
            }
            
            result.put("contents", contentsList);
            result.put("path", dirPath);
            
            return successMessage(result);
            
        } catch (GHFileNotFoundException e) {
            return errorMessage("Directory or repository not found: " + e.getMessage());
        } catch (IOException e) {
            return errorMessage("IO error: " + e.getMessage());
        } catch (Exception e) {
            return errorMessage("Unexpected error: " + e.getMessage());
        }
    }

    private Map<String, Object> getContentDetails(GHContent content) {
        Map<String, Object> contentData = new HashMap<>();
        contentData.put("name", content.getName());
        contentData.put("path", content.getPath());
        contentData.put("sha", content.getSha());
        contentData.put("size", content.getSize());
        return contentData;
    }

    @Tool(description = """
    Create or update a file in a repository.
    If the file doesn't exist, it will be created. If it exists, it will be updated.
    """)
    public String createOrUpdateFile(
            @ToolParam(description = "Repository name in format 'owner/repo'", required = false) String repository,
            @ToolParam(description = "Path to the file in the repository") String path,
            @ToolParam(description = "File content") String content,
            @ToolParam(description = "Commit message") String message,
            @ToolParam(description = "Branch name (defaults to the default branch)", required = false) String branch,
            @ToolParam(description = "Current file SHA (required for updates, not for new files)", required = false) String sha
    ) {
        Map<String, Object> result = new HashMap<>();
        
        try {
            if (path == null || path.isEmpty()) {
                return errorMessage("File path is required");
            }
            
            if (content == null) {
                return errorMessage("File content is required");
            }
            
            if (message == null || message.isEmpty()) {
                return errorMessage("Commit message is required");
            }
            
            Optional<GitHubEnv> env = GitHubClientFactory.getEnvironment();
            if (env.isEmpty()) {
                return errorMessage("GitHub is not configured correctly");
            }
            
            GitHubEnv githubEnv = env.get();
            GitHub github = GitHubClientFactory.createClient(githubEnv);
            
            // Use provided repository or default from environment
            String repoName = (repository != null && !repository.isEmpty()) ? 
                                repository : githubEnv.getFullRepository();
                                
            if (repoName == null || repoName.isEmpty()) {
                return errorMessage("Repository name is required");
            }
            
            GHRepository repo = github.getRepository(repoName);
            
            // Determine branch to use
            String branchToUse = (branch != null && !branch.isEmpty()) ? 
                                  branch : repo.getDefaultBranch();
            
            // Create GHContentBuilder
            GHContentBuilder contentBuilder = repo.createContent()
                    .content(content)
                    .message(message)
                    .path(path)
                    .branch(branchToUse);
            
            // Add SHA if updating an existing file
            if (sha != null && !sha.isEmpty()) {
                contentBuilder.sha(sha);
            }
            
            // Commit the changes
            GHContentUpdateResponse response = contentBuilder.commit();
            
            // Prepare response data
            Map<String, Object> contentData = new HashMap<>();
            contentData.put("path", path);
            
            // Get the commit info
            GitCommit commit = response.getCommit();
            Map<String, Object> commitData = new HashMap<>();
            commitData.put("sha", commit.getSHA1());
            commitData.put("url", commit.getHtmlUrl());
            commitData.put("message", commit.getMessage());
            contentData.put("commit", commitData);
            
            // Get the content info
            GHContent fileContent = response.getContent();
            contentData.put("sha", fileContent.getSha());
            contentData.put("name", fileContent.getName());
            contentData.put("url", fileContent.getHtmlUrl());
            
            result.put("operation", sha != null ? "update" : "create");
            result.put("file", contentData);
            
            return successMessage(result);
            
        } catch (GHFileNotFoundException e) {
            return errorMessage("Repository not found: " + e.getMessage());
        } catch (IOException e) {
            return errorMessage("IO error: " + e.getMessage());
        } catch (Exception e) {
            return errorMessage("Unexpected error: " + e.getMessage());
        }
    }

    @Tool(description = """
    Search for code within repositories.
    Searches GitHub for code matching the query.
    """)
    public String searchCode(
            @ToolParam(description = "Search query") String query,
            @ToolParam(description = "Repository name in format 'owner/repo' to limit search", required = false) String repository,
            @ToolParam(description = "Filter by file extension (e.g., 'java', 'py')", required = false) String extension,
            @ToolParam(description = "Maximum number of results to return", required = false) Integer limit
    ) {
        Map<String, Object> result = new HashMap<>();
        
        try {
            if (query == null || query.isEmpty()) {
                return errorMessage("Search query is required");
            }
            
            Optional<GitHubEnv> env = GitHubClientFactory.getEnvironment();
            if (env.isEmpty()) {
                return errorMessage("GitHub is not configured correctly");
            }
            
            GitHubEnv githubEnv = env.get();
            GitHub github = GitHubClientFactory.createClient(githubEnv);
            
            // Build search query
            StringBuilder queryBuilder = new StringBuilder(query);
            
            // Add repository filter if provided
            if (repository != null && !repository.isEmpty()) {
                queryBuilder.append(" repo:").append(repository);
            }
            
            // Add extension filter if provided
            if (extension != null && !extension.isEmpty()) {
                queryBuilder.append(" extension:").append(extension);
            }
            
            int actualLimit = (limit != null && limit > 0) ? limit : 20; // Default to 20 results
            
            GHContentSearchBuilder searchBuilder = github.searchContent()
                    .q(queryBuilder.toString());
            
            List<Map<String, Object>> resultsList = new ArrayList<>();
            int count = 0;
            
            for (GHContent content : searchBuilder.list().withPageSize(actualLimit)) {
                if (count >= actualLimit) {
                    break;
                }
                
                Map<String, Object> contentData = new HashMap<>();
                contentData.put("name", content.getName());
                contentData.put("path", content.getPath());
                contentData.put("sha", content.getSha());
                contentData.put("repository", content.getOwner().getFullName());
                contentData.put("html_url", content.getHtmlUrl());
                
                // Try to get a snippet of content for context
                try {
                    // The content is base64 encoded
                    String base64Content = content.getContent();
                    if (base64Content != null) {
                        String decodedContent = new String(Base64.getDecoder().decode(base64Content), StandardCharsets.UTF_8);
                        
                        // Get a snippet (first 200 chars or less)
                        int snippetLength = Math.min(decodedContent.length(), 200);
                        String snippet = decodedContent.substring(0, snippetLength);
                        if (snippetLength < decodedContent.length()) {
                            snippet += "...";
                        }
                        
                        contentData.put("text_matches", snippet);
                    }
                } catch (Exception e) {
                    // Ignore content retrieval errors for search results
                    contentData.put("text_matches", "[Content unavailable]");
                }
                
                resultsList.add(contentData);
                count++;
            }
            
            result.put("items", resultsList);
            result.put("count", resultsList.size());
            result.put("query", queryBuilder.toString());
            
            return successMessage(result);
            
        } catch (IOException e) {
            return errorMessage("IO error: " + e.getMessage());
        } catch (Exception e) {
            return errorMessage("Unexpected error: " + e.getMessage());
        }
    }
}

```

--------------------------------------------------------------------------------
/src/main/java/com/devoxx/agentic/github/tools/IssueService.java:
--------------------------------------------------------------------------------

```java
package com.devoxx.agentic.github.tools;

import org.kohsuke.github.*;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.*;

/**
 * Service for GitHub issue-related operations
 */
@Service
public class IssueService extends AbstractToolService {

    @Tool(description = """
        List issues for a repository.
        Returns issues with filtering options for state, labels, and more.
    """)
    public String listIssues(
            @ToolParam(description = "Repository name in format 'owner/repo'", required = false) String repository,
            @ToolParam(description = "State of issues to return (open, closed, all)", required = false) String state,
            @ToolParam(description = "Maximum number of issues to return", required = false) Integer limit
    ) {
        Map<String, Object> result = new HashMap<>();
        
        try {
            Optional<GitHubEnv> env = GitHubClientFactory.getEnvironment();
            if (env.isEmpty()) {
                return errorMessage("GitHub is not configured correctly");
            }
            
            GitHubEnv githubEnv = env.get();
            GitHub github = GitHubClientFactory.createClient(githubEnv);
            
            // Use provided repository or default from environment
            String repoName = (repository != null && !repository.isEmpty()) ? 
                                repository : githubEnv.getFullRepository();
                                
            if (repoName == null || repoName.isEmpty()) {
                return errorMessage("Repository name is required");
            }
            
            GHRepository repo = github.getRepository(repoName);
            
            GHIssueState issueState = GHIssueState.OPEN;
            if (state != null) {
                issueState = switch (state.toLowerCase()) {
                    case "closed" -> GHIssueState.CLOSED;
                    case "all" -> GHIssueState.ALL;
                    default -> issueState;
                };
            }
            
            List<GHIssue> issues;
            
//            if (labels != null && !labels.isEmpty()) {
//                String[] labelArray = labels.split(",");
//                issues = repo.getIssues(issueState, labelArray);
//            } else {
                issues = repo.getIssues(issueState);
//            }
            
            List<Map<String, Object>> issueList = new ArrayList<>();
            int count = 0;
            
            for (GHIssue issue : issues) {
                if (limit != null && count >= limit) {
                    break;
                }
                
                Map<String, Object> issueData = new HashMap<>();
                issueData.put("number", issue.getNumber());
                issueData.put("title", issue.getTitle());
                issueData.put("state", issue.getState().name().toLowerCase());
                issueData.put("html_url", issue.getHtmlUrl().toString());
                
                List<String> issueLabels = new ArrayList<>();
                for (GHLabel label : issue.getLabels()) {
                    issueLabels.add(label.getName());
                }
                issueData.put("labels", issueLabels);
                
                issueData.put("created_at", issue.getCreatedAt().toString());
                issueData.put("updated_at", issue.getUpdatedAt().toString());
                issueData.put("closed_at", issue.getClosedAt() != null ? issue.getClosedAt().toString() : null);
                
                if (issue.getAssignee() != null) {
                    issueData.put("assignee", issue.getAssignee().getLogin());
                }
                
                issueList.add(issueData);
                count++;
            }
            
            result.put("issues", issueList);
            result.put("total_count", issues.size());
            
            return successMessage(result);
            
        } catch (GHFileNotFoundException e) {
            return errorMessage("Repository not found: " + e.getMessage());
        } catch (IOException e) {
            return errorMessage("IO error: " + e.getMessage());
        } catch (Exception e) {
            return errorMessage("Unexpected error: " + e.getMessage());
        }
    }

    @Tool(description = """
        Get a specific issue in a repository.
        Returns detailed information about the issue.
    """)
    public String getIssue(
            @ToolParam(description = "Repository name in format 'owner/repo'", required = false) String repository,
            @ToolParam(description = "Issue number") Integer issueNumber
    ) {
        Map<String, Object> result = new HashMap<>();
        
        try {
            if (issueNumber == null) {
                return errorMessage("Issue number is required");
            }
            
            Optional<GitHubEnv> env = GitHubClientFactory.getEnvironment();
            if (env.isEmpty()) {
                return errorMessage("GitHub is not configured correctly");
            }
            
            GitHubEnv githubEnv = env.get();
            GitHub github = GitHubClientFactory.createClient(githubEnv);
            
            // Use provided repository or default from environment
            String repoName = (repository != null && !repository.isEmpty()) ? 
                                repository : githubEnv.getFullRepository();
                                
            if (repoName == null || repoName.isEmpty()) {
                return errorMessage("Repository name is required");
            }
            
            GHRepository repo = github.getRepository(repoName);
            GHIssue issue = repo.getIssue(issueNumber);
            
            Map<String, Object> issueData = new HashMap<>();
            issueData.put("number", issue.getNumber());
            issueData.put("title", issue.getTitle());
            issueData.put("body", issue.getBody());
            issueData.put("state", issue.getState().name().toLowerCase());
            issueData.put("html_url", issue.getHtmlUrl().toString());
            
            List<String> labels = new ArrayList<>();
            for (GHLabel label : issue.getLabels()) {
                labels.add(label.getName());
            }
            issueData.put("labels", labels);
            
            issueData.put("created_at", issue.getCreatedAt().toString());
            issueData.put("updated_at", issue.getUpdatedAt().toString());
            issueData.put("closed_at", issue.getClosedAt() != null ? issue.getClosedAt().toString() : null);
            
            if (issue.getAssignee() != null) {
                issueData.put("assignee", issue.getAssignee().getLogin());
            }
            
            // Get comments
            List<Map<String, Object>> commentsList = new ArrayList<>();
            for (GHIssueComment comment : issue.getComments()) {
                Map<String, Object> commentData = new HashMap<>();
                commentData.put("id", comment.getId());
                commentData.put("user", comment.getUser().getLogin());
                commentData.put("body", comment.getBody());
                commentData.put("created_at", comment.getCreatedAt().toString());
                commentData.put("updated_at", comment.getUpdatedAt().toString());
                
                commentsList.add(commentData);
            }
            
            issueData.put("comments", commentsList);
            issueData.put("comments_count", commentsList.size());
            
            result.put("issue", issueData);
            
            return successMessage(result);
            
        } catch (GHFileNotFoundException e) {
            return errorMessage("Issue or repository not found: " + e.getMessage());
        } catch (IOException e) {
            return errorMessage("IO error: " + e.getMessage());
        } catch (Exception e) {
            return errorMessage("Unexpected error: " + e.getMessage());
        }
    }

    @Tool(description = """
    Create a new issue in a repository.
    Creates an issue with the specified title, body, and optional labels.
    """)
    public String createIssue(
            @ToolParam(description = "Repository name in format 'owner/repo'", required = false) String repository,
            @ToolParam(description = "Issue title") String title,
            @ToolParam(description = "Issue body/description", required = false) String body,
            @ToolParam(description = "Comma-separated list of labels", required = false) String labels
    ) {
        Map<String, Object> result = new HashMap<>();
        
        try {
            if (title == null || title.isEmpty()) {
                return errorMessage("Issue title is required");
            }
            
            Optional<GitHubEnv> env = GitHubClientFactory.getEnvironment();
            if (env.isEmpty()) {
                return errorMessage("GitHub is not configured correctly");
            }
            
            GitHubEnv githubEnv = env.get();
            GitHub github = GitHubClientFactory.createClient(githubEnv);
            
            // Use provided repository or default from environment
            String repoName = (repository != null && !repository.isEmpty()) ? 
                                repository : githubEnv.getFullRepository();
                                
            if (repoName == null || repoName.isEmpty()) {
                return errorMessage("Repository name is required");
            }
            
            GHRepository repo = github.getRepository(repoName);
            
            GHIssueBuilder issueBuilder = repo.createIssue(title);
            
            if (body != null && !body.isEmpty()) {
                issueBuilder.body(body);
            }
            
            if (labels != null && !labels.isEmpty()) {
                String[] labelArray = labels.split(",");
                for (String label : labelArray) {
                    issueBuilder.label(label.trim());
                }
            }
            
            GHIssue issue = issueBuilder.create();
            
            Map<String, Object> issueData = new HashMap<>();
            issueData.put("number", issue.getNumber());
            issueData.put("title", issue.getTitle());
            issueData.put("body", issue.getBody());
            issueData.put("html_url", issue.getHtmlUrl().toString());
            
            result.put("issue", issueData);
            
            return successMessage(result);
            
        } catch (GHFileNotFoundException e) {
            return errorMessage("Repository not found: " + e.getMessage());
        } catch (IOException e) {
            return errorMessage("IO error: " + e.getMessage());
        } catch (Exception e) {
            return errorMessage("Unexpected error: " + e.getMessage());
        }
    }

    @Tool(description = """
    Add a comment to an issue.
    Posts a new comment on the specified issue.
    """)
    public String addIssueComment(
            @ToolParam(description = "Repository name in format 'owner/repo'", required = false) String repository,
            @ToolParam(description = "Issue number") Integer issueNumber,
            @ToolParam(description = "Comment text") String body
    ) {
        Map<String, Object> result = new HashMap<>();
        
        try {
            if (issueNumber == null) {
                return errorMessage("Issue number is required");
            }
            
            if (body == null || body.isEmpty()) {
                return errorMessage("Comment body is required");
            }
            
            Optional<GitHubEnv> env = GitHubClientFactory.getEnvironment();
            if (env.isEmpty()) {
                return errorMessage("GitHub is not configured correctly");
            }
            
            GitHubEnv githubEnv = env.get();
            GitHub github = GitHubClientFactory.createClient(githubEnv);
            
            // Use provided repository or default from environment
            String repoName = (repository != null && !repository.isEmpty()) ? 
                                repository : githubEnv.getFullRepository();
                                
            if (repoName == null || repoName.isEmpty()) {
                return errorMessage("Repository name is required");
            }
            
            GHRepository repo = github.getRepository(repoName);
            GHIssue issue = repo.getIssue(issueNumber);
            
            GHIssueComment comment = issue.comment(body);
            
            Map<String, Object> commentData = new HashMap<>();
            commentData.put("id", comment.getId());
            commentData.put("body", comment.getBody());
            commentData.put("html_url", comment.getHtmlUrl().toString());
            commentData.put("created_at", comment.getCreatedAt().toString());
            
            result.put("comment", commentData);
            
            return successMessage(result);
            
        } catch (GHFileNotFoundException e) {
            return errorMessage("Issue or repository not found: " + e.getMessage());
        } catch (IOException e) {
            return errorMessage("IO error: " + e.getMessage());
        } catch (Exception e) {
            return errorMessage("Unexpected error: " + e.getMessage());
        }
    }
    
    @Tool(description = """
    Search for issues.
    Searches for issues matching the query across GitHub or in a specific repository.
    """)
    public String searchIssues(
            @ToolParam(description = "Search query") String query,
            @ToolParam(description = "Repository name in format 'owner/repo' to limit search", required = false) String repository,
            @ToolParam(description = "State of issues to search for (open, closed)", required = false) String state,
            @ToolParam(description = "Maximum number of results to return", required = false) Integer limit
    ) {
        Map<String, Object> result = new HashMap<>();
        
        try {
            if (query == null || query.isEmpty()) {
                return errorMessage("Search query is required");
            }
            
            Optional<GitHubEnv> env = GitHubClientFactory.getEnvironment();
            if (env.isEmpty()) {
                return errorMessage("GitHub is not configured correctly");
            }
            
            GitHubEnv githubEnv = env.get();
            GitHub github = GitHubClientFactory.createClient(githubEnv);
            
            // Build search query
            StringBuilder queryBuilder = new StringBuilder(query);
            
            // Add repository filter if provided
            if (repository != null && !repository.isEmpty()) {
                queryBuilder.append(" repo:").append(repository);
            }
            
            // Add state filter if provided
            if (state != null && !state.isEmpty()) {
                queryBuilder.append(" is:").append(state);
            }
            
            int actualLimit = (limit != null && limit > 0) ? limit : 10;
            
            GHIssueSearchBuilder searchBuilder = github.searchIssues()
                    .q(queryBuilder.toString())
                    .order(GHDirection.DESC)
                    .sort(GHIssueSearchBuilder.Sort.CREATED);
            
            List<Map<String, Object>> issueList = new ArrayList<>();
            
            searchBuilder.list().withPageSize(actualLimit).iterator().forEachRemaining(issue -> {
                if (issueList.size() < actualLimit) {
                    Map<String, Object> issueData = new HashMap<>();
                    issueData.put("number", issue.getNumber());
                    issueData.put("title", issue.getTitle());
                    issueData.put("state", issue.getState().name().toLowerCase());
                    issueData.put("repository", issue.getRepository().getFullName());
                    issueData.put("html_url", issue.getHtmlUrl().toString());
                    try {
                        issueData.put("created_at", issue.getCreatedAt().toString());
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }

                    issueList.add(issueData);
                }
            });
            
            result.put("issues", issueList);
            result.put("query", queryBuilder.toString());
            
            return successMessage(result);
            
        } catch (IOException e) {
            return errorMessage("IO error: " + e.getMessage());
        } catch (Exception e) {
            return errorMessage("Unexpected error: " + e.getMessage());
        }
    }
}

```