# 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:
--------------------------------------------------------------------------------
```
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Editor-based HTTP Client requests
5 | /httpRequests/
6 | # Datasource local storage ignored files
7 | /dataSources/
8 | /dataSources.local.xml
9 | 
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
 1 | target/
 2 | !.mvn/wrapper/maven-wrapper.jar
 3 | !**/src/main/**/target/
 4 | !**/src/test/**/target/
 5 | 
 6 | ### IntelliJ IDEA ###
 7 | .idea/modules.xml
 8 | .idea/jarRepositories.xml
 9 | .idea/compiler.xml
10 | .idea/libraries/
11 | *.iws
12 | *.iml
13 | *.ipr
14 | 
15 | ### Eclipse ###
16 | .apt_generated
17 | .classpath
18 | .factorypath
19 | .project
20 | .settings
21 | .springBeans
22 | .sts4-cache
23 | 
24 | ### NetBeans ###
25 | /nbproject/private/
26 | /nbbuild/
27 | /dist/
28 | /nbdist/
29 | /.nb-gradle/
30 | build/
31 | !**/src/main/**/build/
32 | !**/src/test/**/build/
33 | 
34 | ### VS Code ###
35 | .vscode/
36 | 
37 | ### Mac OS ###
38 | .DS_Store
39 | /.idea/
40 | /.env
41 | 
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
  1 | # GitHub MCP Server
  2 | 
  3 | 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.
  4 | 
  5 | ## Features
  6 | 
  7 | The server provides the following GitHub operations:
  8 | 
  9 | - **Repository Management**
 10 |   - List repositories for the authenticated user
 11 |   - Get repository details
 12 |   - Search for repositories
 13 | 
 14 | - **Issue Management**
 15 |   - List issues with filtering options
 16 |   - Get detailed information about issues
 17 |   - Create new issues
 18 |   - Add comments to issues
 19 |   - Search for issues
 20 | 
 21 | - **Pull Request Management**
 22 |   - List pull requests with filtering options
 23 |   - Get detailed information about pull requests
 24 |   - Create comments on pull requests
 25 |   - Merge pull requests
 26 | 
 27 | - **Branch Management**
 28 |   - List branches in a repository
 29 |   - Create new branches
 30 | 
 31 | - **Commit Management**
 32 |   - Get detailed information about commits
 33 |   - List commits with filtering options
 34 |   - Search for commits by message
 35 | 
 36 | - **Content Management**
 37 |   - Get file contents from repositories
 38 |   - List directory contents
 39 |   - Create or update files
 40 |   - Search for code within repositories
 41 | 
 42 | 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.
 43 | 
 44 | ### Claude Desktop
 45 | <img width="931" alt="GitHubExample" src="https://github.com/user-attachments/assets/aa39e8d3-7443-41c7-888f-640680a3a297" />
 46 | 
 47 | ### DevoxxGenie IDEA plugin
 48 | <img width="894" alt="Screenshot 2025-04-11 at 12 32 58" src="https://github.com/user-attachments/assets/06a66881-0ca6-494e-814d-cafe97e870cc" />
 49 | 
 50 | 
 51 | ## Getting Started
 52 | 
 53 | ### Prerequisites
 54 | 
 55 | - Java 17 or higher
 56 | - Maven 3.6+
 57 | - Spring Boot 3.3.6
 58 | - Spring AI MCP Server components
 59 | - A GitHub account and personal access token
 60 | 
 61 | ### Building the Project
 62 | 
 63 | Build the project using Maven:
 64 | 
 65 | ```bash
 66 | mvn clean package
 67 | ```
 68 | 
 69 | ### Running the Server
 70 | 
 71 | Run the server using the following command:
 72 | 
 73 | ```bash
 74 | java -jar target/GitHubMCP-1.0-SNAPSHOT.jar
 75 | ```
 76 | 
 77 | The server can use STDIO for communication with MCP clients or can be run as a web server.
 78 | 
 79 | ## Environment Variables
 80 | 
 81 | The GitHub MCP server supports the following environment variables for authentication:
 82 | 
 83 | - `GITHUB_TOKEN` or `GITHUB_PERSONAL_ACCESS_TOKEN`: Your GitHub personal access token
 84 | - `GITHUB_HOST`: The base URL of your GitHub instance (e.g., `github.com` or `github.mycompany.com` for GitHub Enterprise)
 85 | - `GITHUB_REPOSITORY`: Default repository to use if not specified in API calls (e.g., `owner/repo`)
 86 | 
 87 | 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.
 88 | 
 89 | ## Usage with MCP Clients
 90 | 
 91 | ### Using with Claude Desktop
 92 | 
 93 | Edit your claude_desktop_config.json file with the following:
 94 | 
 95 | ```json
 96 | {
 97 |   "mcpServers": {
 98 |     "github": {
 99 |       "command": "java",
100 |       "args": [
101 |         "-Dspring.ai.mcp.server.stdio=true",
102 |         "-Dspring.main.web-application-type=none",
103 |         "-Dlogging.pattern.console=",
104 |         "-jar",
105 |         "/path/to/GitHubMCP/target/GitHubMCP-1.0-SNAPSHOT.jar"
106 |       ],
107 |       "env": {
108 |         "GITHUB_TOKEN": "your-github-token-here",
109 |         "GITHUB_HOST": "github.com",
110 |         "GITHUB_REPOSITORY": "your-username/your-repository"
111 |       }
112 |     }  
113 |   }
114 | }
115 | ```
116 | 
117 | ### Using with DevoxxGenie or similar MCP clients
118 | 
119 | 1. In your MCP client, access the MCP Server configuration screen
120 | 2. Configure the server with the following settings:
121 |    - **Name**: `GitHub` (or any descriptive name)
122 |    - **Transport Type**: `STDIO`
123 |    - **Command**: Full path to your Java executable
124 |    - **Arguments**:
125 |      ```
126 |      -Dspring.ai.mcp.server.stdio=true
127 |      -Dspring.main.web-application-type=none
128 |      -Dlogging.pattern.console=
129 |      -jar
130 |      /path/to/GitHubMCP/target/GitHubMCP-1.0-SNAPSHOT.jar
131 |      ```
132 |    - **Environment Variables**:
133 |      ```
134 |      GITHUB_TOKEN=your-github-token-here
135 |      GITHUB_HOST=github.com
136 |      GITHUB_REPOSITORY=your-username/your-repository
137 |      ```
138 | 
139 | ## Security Considerations
140 | 
141 | When using this server, be aware that:
142 | - You need to provide a GitHub personal access token for authentication
143 | - The LLM agent will have access to create, read, and modify GitHub resources
144 | - Consider running the server with appropriate permissions and in a controlled environment
145 | - Ensure your token has only the minimum required permissions for your use case
146 | 
147 | ## Example Usage
148 | 
149 | ### With Environment Variables
150 | 
151 | If you've configured the MCP server with environment variables, you can simply ask:
152 | 
153 | ```
154 | Can you get a list of open issues in my GitHub repository?
155 | ```
156 | 
157 | Claude will use the GitHub services with the pre-configured authentication details to fetch the open issues from your GitHub repository and summarize them.
158 | 
159 | ### With Explicit Repository
160 | 
161 | You can override the default repository by specifying it in your request:
162 | 
163 | ```
164 | Can you list the pull requests for the anthropics/claude-playground repository?
165 | ```
166 | 
167 | The GitHub service will use your authentication token but query the specified repository instead of the default one.
168 | 
169 | ## Available Services
170 | 
171 | The GitHub MCP server is organized into several service classes, each providing different functionality:
172 | 
173 | 1. **RepositoryService**
174 |    - `listRepositories`: List repositories for the authenticated user
175 |    - `getRepository`: Get detailed information about a specific repository
176 |    - `searchRepositories`: Search for repositories matching a query
177 | 
178 | 2. **IssueService**
179 |    - `listIssues`: List issues for a repository with filtering options
180 |    - `getIssue`: Get detailed information about a specific issue
181 |    - `createIssue`: Create a new issue in a repository
182 |    - `addIssueComment`: Add a comment to an issue
183 |    - `searchIssues`: Search for issues matching a query
184 | 
185 | 3. **PullRequestService**
186 |    - `listPullRequests`: List pull requests for a repository with filtering options
187 |    - `getPullRequest`: Get detailed information about a specific pull request
188 |    - `createPullRequestComment`: Add a comment to a pull request
189 |    - `mergePullRequest`: Merge a pull request with specified merge method
190 | 
191 | 4. **BranchService**
192 |    - `listBranches`: List branches in a repository with filtering options
193 |    - `createBranch`: Create a new branch from a specified reference
194 | 
195 | 5. **CommitService**
196 |    - `getCommitDetails`: Get detailed information about a specific commit
197 |    - `listCommits`: List commits in a repository with filtering options
198 |    - `findCommitByMessage`: Search for commits containing specific text in their messages
199 | 
200 | 6. **ContentService**
201 |    - `getFileContents`: Get the contents of a file in a repository
202 |    - `listDirectoryContents`: List contents of a directory in a repository
203 |    - `createOrUpdateFile`: Create or update a file in a repository
204 |    - `searchCode`: Search for code within repositories
205 | 
206 | 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.
207 | 
208 | ## Enterprise GitHub Support
209 | 
210 | 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.
211 | 
212 | ## Limitations
213 | 
214 | - The server requires a valid GitHub personal access token with appropriate permissions
215 | - Rate limiting is subject to GitHub API limits
216 | - Some operations may require specific permissions on the token
217 | - Large repositories or files may encounter performance limitations
218 | 
219 | ## Contributing
220 | 
221 | Contributions are welcome! Please feel free to submit a Pull Request.
222 | 
223 | ## License
224 | 
225 | This project is licensed under the MIT License - see the LICENSE file for details.
226 | 
```
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
```
1 | <?xml version="1.0" encoding="UTF-8"?>
2 | <project version="4">
3 |   <component name="VcsDirectoryMappings">
4 |     <mapping directory="$PROJECT_DIR$" vcs="Git" />
5 |   </component>
6 | </project>
```
--------------------------------------------------------------------------------
/.idea/encodings.xml:
--------------------------------------------------------------------------------
```
1 | <?xml version="1.0" encoding="UTF-8"?>
2 | <project version="4">
3 |   <component name="Encoding">
4 |     <file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
5 |     <file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
6 |   </component>
7 | </project>
```
--------------------------------------------------------------------------------
/src/main/resources/application.properties:
--------------------------------------------------------------------------------
```
 1 | # Application settings for GitHub MCP server
 2 | # NOTE: You must disable the banner and the console logging 
 3 | # to allow the STDIO transport to work !!!
 4 | spring.main.banner-mode=off
 5 | logging.pattern.console=
 6 | 
 7 | # MCP server configuration
 8 | spring.ai.mcp.server.name=github-server
 9 | spring.ai.mcp.server.version=0.0.1
10 | 
11 | # spring.ai.mcp.server.stdio=true
12 | 
13 | # Web server configuration (disable for STDIO mode)
14 | # spring.main.web-application-type=none
15 | 
16 | # Logging configuration
17 | logging.file.name=./target/github-server.log
18 | 
```
--------------------------------------------------------------------------------
/src/main/resources/mcp-servers-config.json:
--------------------------------------------------------------------------------
```json
 1 | {
 2 |   "mcpServers": {
 3 |     "devoxx-github": {
 4 |       "command": "java",
 5 |       "args": [
 6 |         "-Dspring.ai.mcp.server.stdio=true",
 7 |         "-Dspring.main.web-application-type=none",
 8 |         "-Dlogging.pattern.console=",
 9 |         "-jar",
10 |         "/Users/stephan/IdeaProjects/GitHubMCP/target/GitHubMCP-1.0-SNAPSHOT.jar"
11 |       ],
12 |       "env": {
13 |         "GITHUB_TOKEN": "your personal access token",
14 |         "GITHUB_HOST": "github.com",
15 |         "GITHUB_REPOSITORY": "your-username/your-repository"
16 |       }
17 |     }
18 |   }
19 | }
20 | 
```
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
```
 1 | <?xml version="1.0" encoding="UTF-8"?>
 2 | <project version="4">
 3 |   <component name="ExternalStorageConfigurationManager" enabled="true" />
 4 |   <component name="MavenProjectsManager">
 5 |     <option name="originalFiles">
 6 |       <list>
 7 |         <option value="$PROJECT_DIR$/pom.xml" />
 8 |       </list>
 9 |     </option>
10 |   </component>
11 |   <component name="ProjectRootManager" version="2" languageLevel="JDK_23" default="true" project-jdk-name="openjdk-23" project-jdk-type="JavaSDK">
12 |     <output url="file://$PROJECT_DIR$/out" />
13 |   </component>
14 | </project>
```
--------------------------------------------------------------------------------
/src/main/java/com/devoxx/agentic/github/tools/GitHubEnv.java:
--------------------------------------------------------------------------------
```java
 1 | package com.devoxx.agentic.github.tools;
 2 | 
 3 | public record GitHubEnv(String githubToken, String githubHost, String repository) {
 4 |     /**
 5 |      * Returns whether the environment is set up for GitHub Enterprise
 6 |      */
 7 |     public boolean isEnterprise() {
 8 |         return githubHost != null && !githubHost.isEmpty() && 
 9 |                !githubHost.equals("github.com") && !githubHost.equals("https://github.com");
10 |     }
11 |     
12 |     /**
13 |      * Gets the full repository name in owner/repo format
14 |      */
15 |     public String getFullRepository() {
16 |         return repository;
17 |     }
18 | }
19 | 
```
--------------------------------------------------------------------------------
/src/main/java/com/devoxx/agentic/github/tools/AbstractToolService.java:
--------------------------------------------------------------------------------
```java
 1 | package com.devoxx.agentic.github.tools;
 2 | 
 3 | import com.fasterxml.jackson.databind.ObjectMapper;
 4 | 
 5 | import java.util.HashMap;
 6 | import java.util.Map;
 7 | 
 8 | public class AbstractToolService {
 9 | 
10 |     protected static final String SUCCESS = "success";
11 |     protected static final String ERROR = "error";
12 | 
13 |     protected final ObjectMapper mapper = new ObjectMapper();
14 | 
15 |     protected String errorMessage(String errorMessage) {
16 |         Map<String, Object> result = new HashMap<>();
17 |         result.put(SUCCESS, false);
18 |         result.put(ERROR, errorMessage);
19 |         try {
20 |             return mapper.writeValueAsString(result);
21 |         } catch (Exception ex) {
22 |             return "{\"success\": false, \"error\": \"Failed to serialize error result\"}";
23 |         }
24 |     }
25 | 
26 |     protected String successMessage(Map<String, Object> result) {
27 |         result.put(SUCCESS, true);
28 |         try {
29 |             return mapper.writeValueAsString(result);
30 |         } catch (Exception ex) {
31 |             return "{\"success\": false, \"error\": \"Failed to serialize success result\"}";
32 |         }
33 |     }
34 | }
35 | 
```
--------------------------------------------------------------------------------
/src/test/java/com/devoxx/agentic/github/ClientStdio.java:
--------------------------------------------------------------------------------
```java
 1 | package com.devoxx.agentic.github;
 2 | 
 3 | import io.github.cdimascio.dotenv.Dotenv;
 4 | import io.modelcontextprotocol.client.McpClient;
 5 | import io.modelcontextprotocol.client.transport.ServerParameters;
 6 | import io.modelcontextprotocol.client.transport.StdioClientTransport;
 7 | import io.modelcontextprotocol.spec.McpSchema.ListToolsResult;
 8 | 
 9 | public class ClientStdio {
10 | 
11 |     public static void main(String[] args) {
12 |         var stdioParams = ServerParameters.builder("java")
13 |                 .args("-Dspring.ai.mcp.server.stdio=true", 
14 |                       "-Dspring.main.web-application-type=none",
15 |                       "-Dlogging.pattern.console=", 
16 |                       "-jar",
17 |                       "/Users/stephan/IdeaProjects/GitHubMCP/target/GitHubMCP-1.0-SNAPSHOT.jar")
18 |                 .addEnvVar("GITHUB_TOKEN", Dotenv.load().get("GITHUB_TOKEN"))
19 |                 .addEnvVar("GITHUB_HOST", "github.com")
20 |                 .addEnvVar("GITHUB_REPOSITORY", "stephan-dowding/mcp-examples")
21 |                 .build();
22 | 
23 |         var transport = new StdioClientTransport(stdioParams);
24 |         var client = McpClient.sync(transport).build();
25 | 
26 |         client.initialize();
27 | 
28 |         // List and demonstrate tools
29 |         ListToolsResult toolsList = client.listTools();
30 |         System.out.println("Available GitHub MCP Tools = " + toolsList);
31 | 
32 |         client.closeGracefully();
33 |     }
34 | }
35 | 
```
--------------------------------------------------------------------------------
/src/main/java/com/devoxx/agentic/github/GitHubMcpApplication.java:
--------------------------------------------------------------------------------
```java
 1 | package com.devoxx.agentic.github;
 2 | 
 3 | import com.devoxx.agentic.github.tools.BranchService;
 4 | import com.devoxx.agentic.github.tools.CommitService;
 5 | import com.devoxx.agentic.github.tools.ContentService;
 6 | import com.devoxx.agentic.github.tools.IssueService;
 7 | import com.devoxx.agentic.github.tools.PullRequestService;
 8 | import com.devoxx.agentic.github.tools.RepositoryService;
 9 | import org.springframework.ai.tool.ToolCallbackProvider;
10 | import org.springframework.ai.tool.method.MethodToolCallbackProvider;
11 | import org.springframework.boot.SpringApplication;
12 | import org.springframework.boot.autoconfigure.SpringBootApplication;
13 | import org.springframework.context.annotation.Bean;
14 | 
15 | @SpringBootApplication
16 | public class GitHubMcpApplication {
17 | 
18 |     public static void main(String[] args) {
19 |         SpringApplication.run(GitHubMcpApplication.class, args);
20 |     }
21 | 
22 |     @Bean
23 |     public ToolCallbackProvider mcpServices(IssueService issueService,
24 |                                             PullRequestService pullRequestService,
25 |                                             RepositoryService repositoryService,
26 |                                             BranchService branchService,
27 |                                             CommitService commitService,
28 |                                             ContentService contentService) {
29 |         return MethodToolCallbackProvider.builder()
30 |                 .toolObjects(issueService, pullRequestService, repositoryService, branchService, commitService, contentService)
31 |                 .build();
32 |     }
33 | }
34 | 
```
--------------------------------------------------------------------------------
/src/test/java/com/devoxx/agentic/github/GitHubServiceTest.java:
--------------------------------------------------------------------------------
```java
 1 | package com.devoxx.agentic.github;
 2 | 
 3 | import com.devoxx.agentic.github.tools.GitHubClientFactory;
 4 | import com.devoxx.agentic.github.tools.GitHubEnv;
 5 | import io.github.cdimascio.dotenv.Dotenv;
 6 | import org.junit.jupiter.api.Test;
 7 | import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
 8 | import org.kohsuke.github.GitHub;
 9 | import org.kohsuke.github.GHMyself;
10 | 
11 | import java.io.IOException;
12 | import java.util.Optional;
13 | 
14 | import static org.junit.jupiter.api.Assertions.*;
15 | 
16 | @EnabledIfEnvironmentVariable(named = "GITHUB_TOKEN", matches = ".+")
17 | class GitHubServiceTest {
18 | 
19 |     @Test
20 |     void testGitHubConnection() throws IOException {
21 |         String githubToken = Dotenv.load().get("GITHUB_TOKEN");
22 |         String githubHost = Dotenv.load().get("GITHUB_HOST");
23 | 
24 |         System.setProperty("GITHUB_TOKEN", githubToken);
25 |         System.setProperty("GITHUB_HOST", githubHost);
26 | 
27 |         // Test environment setup
28 |         Optional<GitHubEnv> env = GitHubClientFactory.getEnvironment();
29 |         assertTrue(env.isPresent(), "GitHub environment configuration should be present");
30 |         
31 |         GitHubEnv githubEnv = env.get();
32 |         assertNotNull(githubEnv.githubToken(), "GitHub token should not be null");
33 |         assertNotNull(githubEnv.githubHost(), "GitHub host should not be null");
34 |         
35 |         // Test connection to GitHub API
36 |         GitHub github = GitHubClientFactory.createClient(githubEnv);
37 |         assertNotNull(github, "GitHub client should not be null");
38 |         
39 |         // Test that we can get authenticated user
40 |         GHMyself myself = github.getMyself();
41 |         assertNotNull(myself, "GitHub user should not be null");
42 |         assertNotNull(myself.getLogin(), "GitHub username should not be null");
43 |         
44 |         System.out.println("Successfully connected to GitHub as: " + myself.getLogin());
45 |     }
46 | }
47 | 
```
--------------------------------------------------------------------------------
/src/main/java/com/devoxx/agentic/github/tools/GitHubClientFactory.java:
--------------------------------------------------------------------------------
```java
 1 | package com.devoxx.agentic.github.tools;
 2 | 
 3 | import org.kohsuke.github.GitHub;
 4 | import java.io.IOException;
 5 | import java.util.Optional;
 6 | 
 7 | /**
 8 |  * Factory for creating GitHub client instances with proper configuration
 9 |  */
10 | public class GitHubClientFactory {
11 |     
12 |     /**
13 |      * Creates a GitHub client based on the provided environment configuration
14 |      * 
15 |      * @param env The GitHub environment configuration
16 |      * @return A configured GitHub client
17 |      * @throws IOException if there's an error connecting to GitHub
18 |      */
19 |     public static GitHub createClient(GitHubEnv env) throws IOException {
20 |         if (env.isEnterprise()) {
21 |             return GitHub.connectToEnterprise(env.githubHost(), env.githubToken());
22 |         } else {
23 |             return GitHub.connectUsingOAuth(env.githubToken());
24 |         }
25 |     }
26 | 
27 |     /**
28 |      * Retrieves environment variables needed for GitHub API access
29 |      * 
30 |      * @return Optional containing GitHub environment, or empty if configuration is missing
31 |      */
32 |     public static Optional<GitHubEnv> getEnvironment() {
33 |         String token = getEnvValue("GITHUB_TOKEN");
34 |         if (token == null || token.isEmpty()) {
35 |             // Try personal access token alternative env var
36 |             token = getEnvValue("GITHUB_PERSONAL_ACCESS_TOKEN");
37 |             if (token == null || token.isEmpty()) {
38 |                 System.err.println("WARNING: GitHub token not found in environment variables");
39 |                 return Optional.empty();
40 |             }
41 |         }
42 |         
43 |         String host = getEnvValue("GITHUB_HOST");
44 |         if (host == null || host.isEmpty()) {
45 |             // Try GH_HOST alternative
46 |             host = getEnvValue("GH_HOST");
47 |             // Default to github.com if not set
48 |             if (host == null || host.isEmpty()) {
49 |                 host = "github.com";
50 |             }
51 |         }
52 |         
53 |         String repository = getEnvValue("GITHUB_REPOSITORY");
54 |         
55 |         // Log information without exposing token
56 |         String tokenPreview = (token.length() > 8) ? 
57 |             token.substring(0, 4) + "..." + token.substring(token.length() - 4) : "****";
58 |         System.out.println("GitHub configuration: Host=" + host + 
59 |                            ", Token=" + tokenPreview + 
60 |                            ", Default repository=" + repository);
61 |         
62 |         return Optional.of(new GitHubEnv(token, host, repository));
63 |     }
64 |     
65 |     /**
66 |      * Helper method to retrieve environment variables from various sources
67 |      */
68 |     private static String getEnvValue(String name) {
69 |         // First check system properties (useful for tests)
70 |         String value = System.getProperty(name);
71 |         if (value != null && !value.isEmpty()) {
72 |             return value;
73 |         }
74 |         
75 |         // Then check environment variables
76 |         return System.getenv(name);
77 |     }
78 | }
79 | 
```
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
```
  1 | <?xml version="1.0" encoding="UTF-8"?>
  2 | <project xmlns="http://maven.apache.org/POM/4.0.0"
  3 |          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4 |          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  5 |     <modelVersion>4.0.0</modelVersion>
  6 |     <parent>
  7 |         <groupId>org.springframework.boot</groupId>
  8 |         <artifactId>spring-boot-starter-parent</artifactId>
  9 |         <version>3.3.6</version>
 10 |         <relativePath /> <!-- lookup parent from repository -->
 11 |     </parent>
 12 | 
 13 |     <groupId>com.devoxx.agentic</groupId>
 14 |     <artifactId>GitHubMCP</artifactId>
 15 |     <version>1.0-SNAPSHOT</version>
 16 | 
 17 |     <properties>
 18 |         <maven.compiler.source>17</maven.compiler.source>
 19 |         <maven.compiler.target>17</maven.compiler.target>
 20 |         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 21 |     </properties>
 22 | 
 23 |     <dependencyManagement>
 24 |         <dependencies>
 25 |             <dependency>
 26 |                 <groupId>org.springframework.ai</groupId>
 27 |                 <artifactId>spring-ai-bom</artifactId>
 28 |                 <version>1.0.0-SNAPSHOT</version>
 29 |                 <type>pom</type>
 30 |                 <scope>import</scope>
 31 |             </dependency>
 32 |         </dependencies>
 33 |     </dependencyManagement>
 34 | 
 35 |     <dependencies>
 36 |         <dependency>
 37 |             <groupId>org.springframework.ai</groupId>
 38 |             <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
 39 |         </dependency>
 40 |         
 41 |         <dependency>
 42 |             <groupId>org.springframework</groupId>
 43 |             <artifactId>spring-web</artifactId>
 44 |         </dependency>
 45 | 
 46 |         <dependency>
 47 |             <groupId>com.fasterxml.jackson.core</groupId>
 48 |             <artifactId>jackson-databind</artifactId>
 49 |             <version>2.15.2</version>
 50 |         </dependency>
 51 | 
 52 |         <dependency>
 53 |             <groupId>io.github.cdimascio</groupId>
 54 |             <artifactId>dotenv-java</artifactId>
 55 |             <version>3.0.0</version>
 56 |         </dependency>
 57 |         
 58 |         <!-- GitHub API Client -->
 59 |         <dependency>
 60 |             <groupId>org.kohsuke</groupId>
 61 |             <artifactId>github-api</artifactId>
 62 |             <version>1.317</version>
 63 |         </dependency>
 64 | 
 65 |         <!-- Testing dependencies -->
 66 |         <dependency>
 67 |             <groupId>org.junit.jupiter</groupId>
 68 |             <artifactId>junit-jupiter</artifactId>
 69 |             <scope>test</scope>
 70 |         </dependency>
 71 |         
 72 |         <dependency>
 73 |             <groupId>org.mockito</groupId>
 74 |             <artifactId>mockito-junit-jupiter</artifactId>
 75 |             <scope>test</scope>
 76 |         </dependency>
 77 |         
 78 |         <dependency>
 79 |             <groupId>org.mockito</groupId>
 80 |             <artifactId>mockito-core</artifactId>
 81 |             <scope>test</scope>
 82 |         </dependency>
 83 | 
 84 |     </dependencies>
 85 | 
 86 |     <build>
 87 |         <plugins>
 88 |             <plugin>
 89 |                 <groupId>org.springframework.boot</groupId>
 90 |                 <artifactId>spring-boot-maven-plugin</artifactId>
 91 |             </plugin>
 92 |             <plugin>
 93 |                 <groupId>org.apache.maven.plugins</groupId>
 94 |                 <artifactId>maven-surefire-plugin</artifactId>
 95 |             </plugin>
 96 |         </plugins>
 97 |     </build>
 98 | 
 99 |     <repositories>
100 |         <repository>
101 |             <name>Central Portal Snapshots</name>
102 |             <id>central-portal-snapshots</id>
103 |             <url>https://central.sonatype.com/repository/maven-snapshots/</url>
104 |             <releases>
105 |                 <enabled>false</enabled>
106 |             </releases>
107 |             <snapshots>
108 |                 <enabled>true</enabled>
109 |             </snapshots>
110 |         </repository>
111 |         <repository>
112 |             <id>spring-milestones</id>
113 |             <name>Spring Milestones</name>
114 |             <url>https://repo.spring.io/milestone</url>
115 |             <snapshots>
116 |                 <enabled>false</enabled>
117 |             </snapshots>
118 |         </repository>
119 |         <repository>
120 |             <id>spring-snapshots</id>
121 |             <name>Spring Snapshots</name>
122 |             <url>https://repo.spring.io/snapshot</url>
123 |             <releases>
124 |                 <enabled>false</enabled>
125 |             </releases>
126 |         </repository>
127 |     </repositories>
128 | </project>
129 | 
```
--------------------------------------------------------------------------------
/src/main/java/com/devoxx/agentic/github/tools/RepositoryService.java:
--------------------------------------------------------------------------------
```java
  1 | package com.devoxx.agentic.github.tools;
  2 | 
  3 | import org.kohsuke.github.*;
  4 | import org.springframework.ai.tool.annotation.Tool;
  5 | import org.springframework.ai.tool.annotation.ToolParam;
  6 | import org.springframework.stereotype.Service;
  7 | 
  8 | import java.io.IOException;
  9 | import java.util.*;
 10 | 
 11 | /**
 12 |  * Service for GitHub repository-related operations
 13 |  */
 14 | @Service
 15 | public class RepositoryService extends AbstractToolService {
 16 | 
 17 |     @Tool(description = """
 18 |         List repositories for the authenticated user.
 19 |         Returns a list of repositories the user has access to.
 20 |     """)
 21 |     public String listRepositories(
 22 |             @ToolParam(description = "Maximum number of repositories to return", required = false) Integer limit
 23 |     ) {
 24 |         Map<String, Object> result = new HashMap<>();
 25 |         
 26 |         try {
 27 |             Optional<GitHubEnv> env = GitHubClientFactory.getEnvironment();
 28 |             if (env.isEmpty()) {
 29 |                 return errorMessage("GitHub is not configured correctly");
 30 |             }
 31 |             
 32 |             GitHubEnv githubEnv = env.get();
 33 |             GitHub github = GitHubClientFactory.createClient(githubEnv);
 34 |             
 35 |             GHMyself myself = github.getMyself();
 36 |             Map<String, GHRepository> repos = myself.getAllRepositories();
 37 |             
 38 |             List<Map<String, Object>> repoList = new ArrayList<>();
 39 |             int count = 0;
 40 |             
 41 |             for (GHRepository repo : repos.values()) {
 42 |                 if (limit != null && count >= limit) {
 43 |                     break;
 44 |                 }
 45 |                 
 46 |                 Map<String, Object> repoData = new HashMap<>();
 47 |                 repoData.put("name", repo.getName());
 48 |                 repoData.put("full_name", repo.getFullName());
 49 |                 repoData.put("description", repo.getDescription());
 50 |                 repoData.put("url", repo.getHtmlUrl().toString());
 51 |                 repoData.put("stars", repo.getStargazersCount());
 52 |                 repoData.put("forks", repo.listForks().toList().size());
 53 |                 repoData.put("private", repo.isPrivate());
 54 |                 
 55 |                 repoList.add(repoData);
 56 |                 count++;
 57 |             }
 58 |             
 59 |             result.put("repositories", repoList);
 60 |             result.put("total_count", repos.size());
 61 |             
 62 |             return successMessage(result);
 63 |             
 64 |         } catch (IOException e) {
 65 |             return errorMessage("IO error: " + e.getMessage());
 66 |         } catch (Exception e) {
 67 |             return errorMessage("Unexpected error: " + e.getMessage());
 68 |         }
 69 |     }
 70 | 
 71 |     @Tool(description = """
 72 |         Get information about a specific repository.
 73 |         Returns details about the repository such as description, stars, forks, etc.
 74 |     """)
 75 |     public String getRepository(
 76 |             @ToolParam(description = "Repository name in format 'owner/repo'", required = false) String repository
 77 |     ) {
 78 |         Map<String, Object> result = new HashMap<>();
 79 |         
 80 |         try {
 81 |             Optional<GitHubEnv> env = GitHubClientFactory.getEnvironment();
 82 |             if (env.isEmpty()) {
 83 |                 return errorMessage("GitHub is not configured correctly");
 84 |             }
 85 |             
 86 |             GitHubEnv githubEnv = env.get();
 87 |             GitHub github = GitHubClientFactory.createClient(githubEnv);
 88 |             
 89 |             // Use provided repository or default from environment
 90 |             String repoName = (repository != null && !repository.isEmpty()) ? 
 91 |                                 repository : githubEnv.getFullRepository();
 92 |                                 
 93 |             if (repoName == null || repoName.isEmpty()) {
 94 |                 return errorMessage("Repository name is required");
 95 |             }
 96 |             
 97 |             GHRepository repo = github.getRepository(repoName);
 98 |             
 99 |             Map<String, Object> repoData = new HashMap<>();
100 |             repoData.put("name", repo.getName());
101 |             repoData.put("full_name", repo.getFullName());
102 |             repoData.put("description", repo.getDescription());
103 |             repoData.put("url", repo.getHtmlUrl().toString());
104 |             repoData.put("stars", repo.getStargazersCount());
105 |             repoData.put("forks", repo.listForks().toList().size());
106 |             repoData.put("open_issues", repo.getOpenIssueCount());
107 |             repoData.put("watchers", repo.getWatchersCount());
108 |             repoData.put("license", repo.getLicense() != null ? repo.getLicense().getName() : null);
109 |             repoData.put("default_branch", repo.getDefaultBranch());
110 |             repoData.put("created_at", repo.getCreatedAt().toString());
111 |             repoData.put("updated_at", repo.getUpdatedAt().toString());
112 |             repoData.put("private", repo.isPrivate());
113 |             
114 |             result.put("repository", repoData);
115 |             
116 |             return successMessage(result);
117 |             
118 |         } catch (GHFileNotFoundException e) {
119 |             return errorMessage("Repository not found: " + e.getMessage());
120 |         } catch (IOException e) {
121 |             return errorMessage("IO error: " + e.getMessage());
122 |         } catch (Exception e) {
123 |             return errorMessage("Unexpected error: " + e.getMessage());
124 |         }
125 |     }
126 |     
127 |     @Tool(description = """
128 |         Search for repositories.
129 |         Searches GitHub for repositories matching the query.
130 |     """)
131 |     public String searchRepositories(
132 |             @ToolParam(description = "Search query") String query,
133 |             @ToolParam(description = "Maximum number of results to return", required = false) Integer limit
134 |     ) {
135 |         Map<String, Object> result = new HashMap<>();
136 |         
137 |         try {
138 |             if (query == null || query.isEmpty()) {
139 |                 return errorMessage("Search query is required");
140 |             }
141 |             
142 |             Optional<GitHubEnv> env = GitHubClientFactory.getEnvironment();
143 |             if (env.isEmpty()) {
144 |                 return errorMessage("GitHub is not configured correctly");
145 |             }
146 |             
147 |             GitHubEnv githubEnv = env.get();
148 |             GitHub github = GitHubClientFactory.createClient(githubEnv);
149 |             
150 |             int actualLimit = (limit != null && limit > 0) ? limit : 10;
151 |             
152 |             GHRepositorySearchBuilder searchBuilder = github.searchRepositories()
153 |                     .q(query)
154 |                     .order(GHDirection.DESC)
155 |                     .sort(GHRepositorySearchBuilder.Sort.STARS);
156 |             
157 |             List<Map<String, Object>> repoList = new ArrayList<>();
158 |             
159 |             searchBuilder.list().withPageSize(actualLimit).iterator().forEachRemaining(repo -> {
160 |                 if (repoList.size() < actualLimit) {
161 |                     Map<String, Object> repoData = new HashMap<>();
162 |                     repoData.put("name", repo.getName());
163 |                     repoData.put("full_name", repo.getFullName());
164 |                     repoData.put("description", repo.getDescription());
165 |                     repoData.put("url", repo.getHtmlUrl().toString());
166 |                     repoData.put("stars", repo.getStargazersCount());
167 |                     try {
168 |                         repoData.put("forks", repo.listForks().toList().size());
169 |                     } catch (IOException e) {
170 |                         throw new RuntimeException(e);
171 |                     }
172 |                     repoData.put("language", repo.getLanguage());
173 |                     
174 |                     repoList.add(repoData);
175 |                 }
176 |             });
177 |             
178 |             result.put("repositories", repoList);
179 |             result.put("query", query);
180 |             
181 |             return successMessage(result);
182 |             
183 |         } catch (IOException e) {
184 |             return errorMessage("IO error: " + e.getMessage());
185 |         } catch (Exception e) {
186 |             return errorMessage("Unexpected error: " + e.getMessage());
187 |         }
188 |     }
189 | }
190 | 
```
--------------------------------------------------------------------------------
/src/main/java/com/devoxx/agentic/github/tools/BranchService.java:
--------------------------------------------------------------------------------
```java
  1 | package com.devoxx.agentic.github.tools;
  2 | 
  3 | import org.kohsuke.github.*;
  4 | import org.springframework.ai.tool.annotation.Tool;
  5 | import org.springframework.ai.tool.annotation.ToolParam;
  6 | import org.springframework.stereotype.Service;
  7 | 
  8 | import java.io.IOException;
  9 | import java.util.*;
 10 | 
 11 | /**
 12 |  * Service for GitHub branch-related operations
 13 |  */
 14 | @Service
 15 | public class BranchService extends AbstractToolService {
 16 | 
 17 |     @Tool(description = """
 18 |     List branches in a repository.
 19 |     Returns a list of branches with their details.
 20 |     """)
 21 |     public String listBranches(
 22 |             @ToolParam(description = "Repository name in format 'owner/repo'", required = false) String repository,
 23 |             @ToolParam(description = "Filter branches by prefix", required = false) String filter,
 24 |             @ToolParam(description = "Maximum number of branches to return", required = false) Integer limit
 25 |     ) {
 26 |         Map<String, Object> result = new HashMap<>();
 27 |         
 28 |         try {
 29 |             Optional<GitHubEnv> env = GitHubClientFactory.getEnvironment();
 30 |             if (env.isEmpty()) {
 31 |                 return errorMessage("GitHub is not configured correctly");
 32 |             }
 33 |             
 34 |             GitHubEnv githubEnv = env.get();
 35 |             GitHub github = GitHubClientFactory.createClient(githubEnv);
 36 |             
 37 |             // Use provided repository or default from environment
 38 |             String repoName = (repository != null && !repository.isEmpty()) ? 
 39 |                                 repository : githubEnv.getFullRepository();
 40 |                                 
 41 |             if (repoName == null || repoName.isEmpty()) {
 42 |                 return errorMessage("Repository name is required");
 43 |             }
 44 |             
 45 |             GHRepository repo = github.getRepository(repoName);
 46 |             
 47 |             List<Map<String, Object>> branchList = new ArrayList<>();
 48 |             int count = 0;
 49 |             
 50 |             for (GHBranch branch : repo.getBranches().values()) {
 51 |                 // Apply filter if provided
 52 |                 if (filter != null && !filter.isEmpty() && !branch.getName().startsWith(filter)) {
 53 |                     continue;
 54 |                 }
 55 |                 
 56 |                 if (limit != null && count >= limit) {
 57 |                     break;
 58 |                 }
 59 |                 
 60 |                 Map<String, Object> branchData = new HashMap<>();
 61 |                 branchData.put("name", branch.getName());
 62 |                 branchData.put("sha", branch.getSHA1());
 63 |                 
 64 |                 // Get the latest commit for this branch
 65 |                 GHCommit commit = repo.getCommit(branch.getSHA1());
 66 |                 if (commit != null) {
 67 |                     Map<String, Object> commitData = new HashMap<>();
 68 |                     commitData.put("message", commit.getCommitShortInfo().getMessage());
 69 |                     commitData.put("author", commit.getCommitShortInfo().getAuthor().getName());
 70 |                     commitData.put("date", commit.getCommitShortInfo().getCommitDate().toString());
 71 |                     branchData.put("latest_commit", commitData);
 72 |                 }
 73 |                 
 74 |                 // Check if this is the default branch
 75 |                 branchData.put("is_default", branch.getName().equals(repo.getDefaultBranch()));
 76 |                 
 77 |                 // Get protection status (requires separate API call)
 78 |                 try {
 79 |                     GHBranchProtection protection = repo.getBranch(branch.getName()).getProtection();
 80 |                     branchData.put("protected", true);
 81 |                     // Add protection details if needed
 82 |                 } catch (GHFileNotFoundException ex) {
 83 |                     // Branch is not protected
 84 |                     branchData.put("protected", false);
 85 |                 } catch (Exception ex) {
 86 |                     // Ignore other exceptions for protection status
 87 |                     branchData.put("protected", false);
 88 |                 }
 89 |                 
 90 |                 branchList.add(branchData);
 91 |                 count++;
 92 |             }
 93 |             
 94 |             result.put("branches", branchList);
 95 |             result.put("total_count", branchList.size());
 96 |             
 97 |             return successMessage(result);
 98 |             
 99 |         } catch (GHFileNotFoundException e) {
100 |             return errorMessage("Repository not found: " + e.getMessage());
101 |         } catch (IOException e) {
102 |             return errorMessage("IO error: " + e.getMessage());
103 |         } catch (Exception e) {
104 |             return errorMessage("Unexpected error: " + e.getMessage());
105 |         }
106 |     }
107 | 
108 |     @Tool(description = """
109 |     Create a new branch in a repository.
110 |     Creates a branch from a specified SHA or reference (defaults to the default branch if not specified).
111 |     """)
112 |     public String createBranch(
113 |             @ToolParam(description = "Repository name in format 'owner/repo'", required = false) String repository,
114 |             @ToolParam(description = "New branch name") String branchName,
115 |             @ToolParam(description = "SHA or reference to create branch from (defaults to default branch)", required = false) String fromRef
116 |     ) {
117 |         Map<String, Object> result = new HashMap<>();
118 |         
119 |         try {
120 |             if (branchName == null || branchName.isEmpty()) {
121 |                 return errorMessage("Branch name is required");
122 |             }
123 |             
124 |             Optional<GitHubEnv> env = GitHubClientFactory.getEnvironment();
125 |             if (env.isEmpty()) {
126 |                 return errorMessage("GitHub is not configured correctly");
127 |             }
128 |             
129 |             GitHubEnv githubEnv = env.get();
130 |             GitHub github = GitHubClientFactory.createClient(githubEnv);
131 |             
132 |             // Use provided repository or default from environment
133 |             String repoName = (repository != null && !repository.isEmpty()) ? 
134 |                                 repository : githubEnv.getFullRepository();
135 |                                 
136 |             if (repoName == null || repoName.isEmpty()) {
137 |                 return errorMessage("Repository name is required");
138 |             }
139 |             
140 |             GHRepository repo = github.getRepository(repoName);
141 |             
142 |             // Check if branch already exists
143 |             try {
144 |                 GHBranch existingBranch = repo.getBranch(branchName);
145 |                 if (existingBranch != null) {
146 |                     return errorMessage("Branch '" + branchName + "' already exists");
147 |                 }
148 |             } catch (GHFileNotFoundException ex) {
149 |                 // Branch doesn't exist, which is what we want
150 |             }
151 |             
152 |             // Determine the SHA to create from
153 |             String sha;
154 |             if (fromRef != null && !fromRef.isEmpty()) {
155 |                 try {
156 |                     // Try to get the SHA from the reference
157 |                     GHRef ref = repo.getRef("heads/" + fromRef);
158 |                     sha = ref.getObject().getSha();
159 |                 } catch (GHFileNotFoundException ex) {
160 |                     try {
161 |                         // Try to resolve as a direct SHA
162 |                         GHCommit commit = repo.getCommit(fromRef);
163 |                         sha = commit.getSHA1();
164 |                     } catch (Exception e) {
165 |                         return errorMessage("Invalid reference: " + fromRef);
166 |                     }
167 |                 }
168 |             } else {
169 |                 // Use default branch
170 |                 String defaultBranch = repo.getDefaultBranch();
171 |                 GHRef ref = repo.getRef("heads/" + defaultBranch);
172 |                 sha = ref.getObject().getSha();
173 |             }
174 |             
175 |             // Create the new branch
176 |             GHRef newBranch = repo.createRef("refs/heads/" + branchName, sha);
177 |             
178 |             Map<String, Object> branchData = new HashMap<>();
179 |             branchData.put("name", branchName);
180 |             branchData.put("sha", sha);
181 |             branchData.put("url", newBranch.getUrl().toString());
182 |             
183 |             result.put("branch", branchData);
184 |             
185 |             return successMessage(result);
186 |             
187 |         } catch (GHFileNotFoundException e) {
188 |             return errorMessage("Repository not found: " + e.getMessage());
189 |         } catch (IOException e) {
190 |             return errorMessage("IO error: " + e.getMessage());
191 |         } catch (Exception e) {
192 |             return errorMessage("Unexpected error: " + e.getMessage());
193 |         }
194 |     }
195 | }
196 | 
```
--------------------------------------------------------------------------------
/src/main/java/com/devoxx/agentic/github/tools/CommitService.java:
--------------------------------------------------------------------------------
```java
  1 | package com.devoxx.agentic.github.tools;
  2 | 
  3 | import org.kohsuke.github.*;
  4 | import org.springframework.ai.tool.annotation.Tool;
  5 | import org.springframework.ai.tool.annotation.ToolParam;
  6 | import org.springframework.stereotype.Service;
  7 | 
  8 | import java.io.IOException;
  9 | import java.util.*;
 10 | 
 11 | /**
 12 |  * Service for GitHub commit-related operations
 13 |  */
 14 | @Service
 15 | public class CommitService extends AbstractToolService {
 16 | 
 17 |     @Tool(description = """
 18 |         Get detailed information about a specific commit.
 19 |         Returns commit data including author, committer, message, and file changes.
 20 |     """)
 21 |     public String getCommitDetails(
 22 |             @ToolParam(description = "Repository name in format 'owner/repo'", required = false) String repository,
 23 |             @ToolParam(description = "Commit SHA") String sha
 24 |     ) {
 25 |         Map<String, Object> result = new HashMap<>();
 26 |         
 27 |         try {
 28 |             if (sha == null || sha.isEmpty()) {
 29 |                 return errorMessage("Commit SHA is required");
 30 |             }
 31 |             
 32 |             Optional<GitHubEnv> env = GitHubClientFactory.getEnvironment();
 33 |             if (env.isEmpty()) {
 34 |                 return errorMessage("GitHub is not configured correctly");
 35 |             }
 36 |             
 37 |             GitHubEnv githubEnv = env.get();
 38 |             GitHub github = GitHubClientFactory.createClient(githubEnv);
 39 |             
 40 |             // Use provided repository or default from environment
 41 |             String repoName = (repository != null && !repository.isEmpty()) ? 
 42 |                                 repository : githubEnv.getFullRepository();
 43 |                                 
 44 |             if (repoName == null || repoName.isEmpty()) {
 45 |                 return errorMessage("Repository name is required");
 46 |             }
 47 |             
 48 |             GHRepository repo = github.getRepository(repoName);
 49 |             GHCommit commit = repo.getCommit(sha);
 50 |             
 51 |             Map<String, Object> commitData = new HashMap<>();
 52 |             commitData.put("sha", commit.getSHA1());
 53 |             commitData.put("message", commit.getCommitShortInfo().getMessage());
 54 |             commitData.put("html_url", commit.getHtmlUrl().toString());
 55 |             
 56 |             // Author details
 57 |             GHCommit.ShortInfo info = commit.getCommitShortInfo();
 58 |             Map<String, Object> authorData = new HashMap<>();
 59 |             authorData.put("name", info.getAuthor().getName());
 60 |             authorData.put("email", info.getAuthor().getEmail());
 61 |             authorData.put("date", info.getAuthoredDate().toString());
 62 |             commitData.put("author", authorData);
 63 |             
 64 |             // Committer details (might be different from author)
 65 |             Map<String, Object> committerData = new HashMap<>();
 66 |             committerData.put("name", info.getCommitter().getName());
 67 |             committerData.put("email", info.getCommitter().getEmail());
 68 |             committerData.put("date", info.getCommitDate().toString());
 69 |             commitData.put("committer", committerData);
 70 |             
 71 |             // Parents
 72 |             List<Map<String, String>> parentsList = new ArrayList<>();
 73 |             for (GHCommit parent : commit.getParents()) {
 74 |                 Map<String, String> parentData = new HashMap<>();
 75 |                 parentData.put("sha", parent.getSHA1());
 76 |                 parentData.put("url", parent.getHtmlUrl().toString());
 77 |                 parentsList.add(parentData);
 78 |             }
 79 |             commitData.put("parents", parentsList);
 80 |             
 81 |             // File changes
 82 |             List<Map<String, Object>> filesList = new ArrayList<>();
 83 |             for (GHCommit.File file : commit.listFiles()) {
 84 |                 Map<String, Object> fileData = new HashMap<>();
 85 |                 fileData.put("filename", file.getFileName());
 86 |                 fileData.put("status", file.getStatus());
 87 |                 fileData.put("additions", file.getLinesAdded());
 88 |                 fileData.put("deletions", file.getLinesDeleted());
 89 |                 fileData.put("changes", file.getLinesChanged());
 90 |                 fileData.put("patch", file.getPatch());
 91 |                 filesList.add(fileData);
 92 |             }
 93 |             commitData.put("files", filesList);
 94 |             
 95 |             // Stats
 96 |             Map<String, Integer> statsData = new HashMap<>();
 97 |             statsData.put("additions", commit.getLinesAdded());
 98 |             statsData.put("deletions", commit.getLinesDeleted());
 99 |             statsData.put("total", commit.getLinesChanged());
100 |             commitData.put("stats", statsData);
101 |             
102 |             result.put("commit", commitData);
103 |             
104 |             return successMessage(result);
105 |             
106 |         } catch (GHFileNotFoundException e) {
107 |             return errorMessage("Commit or repository not found: " + e.getMessage());
108 |         } catch (IOException e) {
109 |             return errorMessage("IO error: " + e.getMessage());
110 |         } catch (Exception e) {
111 |             return errorMessage("Unexpected error: " + e.getMessage());
112 |         }
113 |     }
114 | 
115 |     @Tool(description = """
116 |     List commits in a repository.
117 |     Returns a list of commits with filtering options for branch and author.
118 |     """)
119 |     public String listCommits(
120 |             @ToolParam(description = "Repository name in format 'owner/repo'", required = false) String repository,
121 |             @ToolParam(description = "Branch or tag name to filter commits", required = false) String branch,
122 |             @ToolParam(description = "Author name or email to filter commits", required = false) String author,
123 |             @ToolParam(description = "Path to filter commits that touch the specified path", required = false) String path,
124 |             @ToolParam(description = "Maximum number of commits to return", required = false) Integer limit
125 |     ) {
126 |         Map<String, Object> result = new HashMap<>();
127 |         
128 |         try {
129 |             Optional<GitHubEnv> env = GitHubClientFactory.getEnvironment();
130 |             if (env.isEmpty()) {
131 |                 return errorMessage("GitHub is not configured correctly");
132 |             }
133 |             
134 |             GitHubEnv githubEnv = env.get();
135 |             GitHub github = GitHubClientFactory.createClient(githubEnv);
136 |             
137 |             // Use provided repository or default from environment
138 |             String repoName = (repository != null && !repository.isEmpty()) ? 
139 |                                 repository : githubEnv.getFullRepository();
140 |                                 
141 |             if (repoName == null || repoName.isEmpty()) {
142 |                 return errorMessage("Repository name is required");
143 |             }
144 |             
145 |             GHRepository repo = github.getRepository(repoName);
146 |             
147 |             // Setup commit query parameters
148 |             GHCommitQueryBuilder queryBuilder = repo.queryCommits();
149 |             
150 |             if (branch != null && !branch.isEmpty()) {
151 |                 queryBuilder.from(branch);
152 |             }
153 |             
154 |             if (author != null && !author.isEmpty()) {
155 |                 queryBuilder.author(author);
156 |             }
157 |             
158 |             if (path != null && !path.isEmpty()) {
159 |                 queryBuilder.path(path);
160 |             }
161 |             
162 |             int actualLimit = (limit != null && limit > 0) ? limit : 30; // Default to 30 commits
163 |             
164 |             List<Map<String, Object>> commitList = new ArrayList<>();
165 |             int count = 0;
166 |             
167 |             for (GHCommit commit : queryBuilder.list().withPageSize(actualLimit)) {
168 |                 if (count >= actualLimit) {
169 |                     break;
170 |                 }
171 |                 
172 |                 Map<String, Object> commitData = new HashMap<>();
173 |                 commitData.put("sha", commit.getSHA1());
174 |                 
175 |                 GHCommit.ShortInfo info = commit.getCommitShortInfo();
176 |                 commitData.put("message", info.getMessage());
177 |                 commitData.put("author", info.getAuthor().getName());
178 |                 commitData.put("author_email", info.getAuthor().getEmail());
179 |                 commitData.put("date", info.getAuthoredDate().toString());
180 |                 
181 |                 // Include stats
182 |                 Map<String, Integer> statsData = new HashMap<>();
183 |                 statsData.put("additions", commit.getLinesAdded());
184 |                 statsData.put("deletions", commit.getLinesDeleted());
185 |                 statsData.put("total", commit.getLinesChanged());
186 |                 commitData.put("stats", statsData);
187 |                 
188 |                 commitData.put("html_url", commit.getHtmlUrl().toString());
189 |                 
190 |                 commitList.add(commitData);
191 |                 count++;
192 |             }
193 |             
194 |             result.put("commits", commitList);
195 |             result.put("count", commitList.size());
196 |             
197 |             if (branch != null && !branch.isEmpty()) {
198 |                 result.put("branch", branch);
199 |             }
200 |             
201 |             if (author != null && !author.isEmpty()) {
202 |                 result.put("author", author);
203 |             }
204 |             
205 |             if (path != null && !path.isEmpty()) {
206 |                 result.put("path", path);
207 |             }
208 |             
209 |             return successMessage(result);
210 |             
211 |         } catch (GHFileNotFoundException e) {
212 |             return errorMessage("Repository not found: " + e.getMessage());
213 |         } catch (IOException e) {
214 |             return errorMessage("IO error: " + e.getMessage());
215 |         } catch (Exception e) {
216 |             return errorMessage("Unexpected error: " + e.getMessage());
217 |         }
218 |     }
219 | 
220 |     @Tool(description = """
221 |     Search for a commit based on the provided text or keywords in the project history.
222 |     Useful for finding specific change sets or code modifications by commit messages or diff content.
223 |     Takes a query parameter and returns the matching commit information.
224 |     Returns matched commit hashes as a JSON array.
225 |     """)
226 |     public String findCommitByMessage(
227 |             @ToolParam(description = "Text to search for in commit messages") String text,
228 |             @ToolParam(description = "Repository name in format 'owner/repo'", required = false) String repository,
229 |             @ToolParam(description = "Branch or tag name to search within", required = false) String branch,
230 |             @ToolParam(description = "Maximum number of results to return", required = false) Integer limit
231 |     ) {
232 |         Map<String, Object> result = new HashMap<>();
233 |         
234 |         try {
235 |             if (text == null || text.isEmpty()) {
236 |                 return errorMessage("Search text is required");
237 |             }
238 |             
239 |             Optional<GitHubEnv> env = GitHubClientFactory.getEnvironment();
240 |             if (env.isEmpty()) {
241 |                 return errorMessage("GitHub is not configured correctly");
242 |             }
243 |             
244 |             GitHubEnv githubEnv = env.get();
245 |             GitHub github = GitHubClientFactory.createClient(githubEnv);
246 |             
247 |             // Use provided repository or default from environment
248 |             String repoName = (repository != null && !repository.isEmpty()) ? 
249 |                                 repository : githubEnv.getFullRepository();
250 |                                 
251 |             if (repoName == null || repoName.isEmpty()) {
252 |                 return errorMessage("Repository name is required");
253 |             }
254 |             
255 |             GHRepository repo = github.getRepository(repoName);
256 |             
257 |             // Setup query parameters
258 |             GHCommitQueryBuilder queryBuilder = repo.queryCommits();
259 |             
260 |             if (branch != null && !branch.isEmpty()) {
261 |                 queryBuilder.from(branch);
262 |             }
263 |             
264 |             // Normalize the search text to lowercase for case-insensitive matching
265 |             String searchText = text.toLowerCase();
266 |             int actualLimit = (limit != null && limit > 0) ? limit : 20; // Default to 20 results
267 |             
268 |             List<Map<String, Object>> matchedCommits = new ArrayList<>();
269 |             int count = 0;
270 |             
271 |             for (GHCommit commit : queryBuilder.list()) {
272 |                 if (count >= actualLimit) {
273 |                     break;
274 |                 }
275 |                 
276 |                 GHCommit.ShortInfo info = commit.getCommitShortInfo();
277 |                 String message = info.getMessage();
278 |                 
279 |                 // Check if the commit message contains the search text
280 |                 if (message != null && message.toLowerCase().contains(searchText)) {
281 |                     Map<String, Object> commitData = new HashMap<>();
282 |                     commitData.put("sha", commit.getSHA1());
283 |                     commitData.put("message", message);
284 |                     commitData.put("author", info.getAuthor().getName());
285 |                     commitData.put("date", info.getAuthoredDate().toString());
286 |                     commitData.put("html_url", commit.getHtmlUrl().toString());
287 |                     
288 |                     // Include stats
289 |                     Map<String, Integer> statsData = new HashMap<>();
290 |                     statsData.put("additions", commit.getLinesAdded());
291 |                     statsData.put("deletions", commit.getLinesDeleted());
292 |                     statsData.put("total", commit.getLinesChanged());
293 |                     commitData.put("stats", statsData);
294 |                     
295 |                     matchedCommits.add(commitData);
296 |                     count++;
297 |                 }
298 |             }
299 |             
300 |             result.put("commits", matchedCommits);
301 |             result.put("count", matchedCommits.size());
302 |             result.put("search_text", text);
303 |             
304 |             if (branch != null && !branch.isEmpty()) {
305 |                 result.put("branch", branch);
306 |             }
307 |             
308 |             return successMessage(result);
309 |             
310 |         } catch (GHFileNotFoundException e) {
311 |             return errorMessage("Repository not found: " + e.getMessage());
312 |         } catch (IOException e) {
313 |             return errorMessage("IO error: " + e.getMessage());
314 |         } catch (Exception e) {
315 |             return errorMessage("Unexpected error: " + e.getMessage());
316 |         }
317 |     }
318 | }
319 | 
```
--------------------------------------------------------------------------------
/src/main/java/com/devoxx/agentic/github/tools/PullRequestService.java:
--------------------------------------------------------------------------------
```java
  1 | package com.devoxx.agentic.github.tools;
  2 | 
  3 | import org.kohsuke.github.*;
  4 | import org.springframework.ai.tool.annotation.Tool;
  5 | import org.springframework.ai.tool.annotation.ToolParam;
  6 | import org.springframework.stereotype.Service;
  7 | 
  8 | import java.io.IOException;
  9 | import java.util.*;
 10 | 
 11 | /**
 12 |  * Service for GitHub pull request-related operations
 13 |  */
 14 | @Service
 15 | public class PullRequestService extends AbstractToolService {
 16 | 
 17 |     @Tool(description = """
 18 |     List pull requests for a repository.
 19 |     Returns pull requests with filtering options for state.
 20 |     """)
 21 |     public String listPullRequests(
 22 |             @ToolParam(description = "Repository name in format 'owner/repo'", required = false) String repository,
 23 |             @ToolParam(description = "State of pull requests (open, closed, all)", required = false) String state,
 24 |             @ToolParam(description = "Maximum number of results to return", required = false) Integer limit
 25 |     ) {
 26 |         Map<String, Object> result = new HashMap<>();
 27 |         
 28 |         try {
 29 |             Optional<GitHubEnv> env = GitHubClientFactory.getEnvironment();
 30 |             if (env.isEmpty()) {
 31 |                 return errorMessage("GitHub is not configured correctly");
 32 |             }
 33 |             
 34 |             GitHubEnv githubEnv = env.get();
 35 |             GitHub github = GitHubClientFactory.createClient(githubEnv);
 36 |             
 37 |             // Use provided repository or default from environment
 38 |             String repoName = (repository != null && !repository.isEmpty()) ? 
 39 |                                 repository : githubEnv.getFullRepository();
 40 |                                 
 41 |             if (repoName == null || repoName.isEmpty()) {
 42 |                 return errorMessage("Repository name is required");
 43 |             }
 44 |             
 45 |             GHRepository repo = github.getRepository(repoName);
 46 |             
 47 |             GHIssueState prState = GHIssueState.OPEN;
 48 |             if (state != null) {
 49 |                 prState = switch (state.toLowerCase()) {
 50 |                     case "closed" -> GHIssueState.CLOSED;
 51 |                     case "all" -> GHIssueState.ALL;
 52 |                     default -> prState;
 53 |                 };
 54 |             }
 55 |             
 56 |             List<GHPullRequest> pullRequests = repo.getPullRequests(prState);
 57 |             List<Map<String, Object>> prList = new ArrayList<>();
 58 |             int count = 0;
 59 |             
 60 |             for (GHPullRequest pr : pullRequests) {
 61 |                 if (limit != null && count >= limit) {
 62 |                     break;
 63 |                 }
 64 |                 
 65 |                 Map<String, Object> prData = new HashMap<>();
 66 |                 prData.put("number", pr.getNumber());
 67 |                 prData.put("title", pr.getTitle());
 68 |                 prData.put("state", pr.getState().name().toLowerCase());
 69 |                 prData.put("html_url", pr.getHtmlUrl().toString());
 70 |                 prData.put("created_at", pr.getCreatedAt().toString());
 71 |                 prData.put("updated_at", pr.getUpdatedAt().toString());
 72 |                 prData.put("closed_at", pr.getClosedAt() != null ? pr.getClosedAt().toString() : null);
 73 |                 prData.put("merged_at", pr.getMergedAt() != null ? pr.getMergedAt().toString() : null);
 74 |                 prData.put("is_merged", pr.isMerged());
 75 |                 
 76 |                 prData.put("user", pr.getUser().getLogin());
 77 |                 
 78 |                 prData.put("base_branch", pr.getBase().getRef());
 79 |                 prData.put("head_branch", pr.getHead().getRef());
 80 |                 
 81 |                 prList.add(prData);
 82 |                 count++;
 83 |             }
 84 |             
 85 |             result.put("pull_requests", prList);
 86 |             result.put("total_count", pullRequests.size());
 87 |             
 88 |             return successMessage(result);
 89 |             
 90 |         } catch (GHFileNotFoundException e) {
 91 |             return errorMessage("Repository not found: " + e.getMessage());
 92 |         } catch (IOException e) {
 93 |             return errorMessage("IO error: " + e.getMessage());
 94 |         } catch (Exception e) {
 95 |             return errorMessage("Unexpected error: " + e.getMessage());
 96 |         }
 97 |     }
 98 | 
 99 |     @Tool(description = """
100 |     Get a specific pull request.
101 |     Returns detailed information about the pull request.
102 |     """)
103 |     public String getPullRequest(
104 |             @ToolParam(description = "Repository name in format 'owner/repo'", required = false) String repository,
105 |             @ToolParam(description = "Pull request number") Integer prNumber
106 |     ) {
107 |         Map<String, Object> result = new HashMap<>();
108 |         
109 |         try {
110 |             if (prNumber == null) {
111 |                 return errorMessage("Pull request number is required");
112 |             }
113 |             
114 |             Optional<GitHubEnv> env = GitHubClientFactory.getEnvironment();
115 |             if (env.isEmpty()) {
116 |                 return errorMessage("GitHub is not configured correctly");
117 |             }
118 |             
119 |             GitHubEnv githubEnv = env.get();
120 |             GitHub github = GitHubClientFactory.createClient(githubEnv);
121 |             
122 |             // Use provided repository or default from environment
123 |             String repoName = (repository != null && !repository.isEmpty()) ? 
124 |                                 repository : githubEnv.getFullRepository();
125 |                                 
126 |             if (repoName == null || repoName.isEmpty()) {
127 |                 return errorMessage("Repository name is required");
128 |             }
129 |             
130 |             GHRepository repo = github.getRepository(repoName);
131 |             GHPullRequest pr = repo.getPullRequest(prNumber);
132 |             
133 |             Map<String, Object> prData = new HashMap<>();
134 |             prData.put("number", pr.getNumber());
135 |             prData.put("title", pr.getTitle());
136 |             prData.put("body", pr.getBody());
137 |             prData.put("state", pr.getState().name().toLowerCase());
138 |             prData.put("html_url", pr.getHtmlUrl().toString());
139 |             prData.put("created_at", pr.getCreatedAt().toString());
140 |             prData.put("updated_at", pr.getUpdatedAt().toString());
141 |             prData.put("closed_at", pr.getClosedAt() != null ? pr.getClosedAt().toString() : null);
142 |             prData.put("merged_at", pr.getMergedAt() != null ? pr.getMergedAt().toString() : null);
143 |             prData.put("is_merged", pr.isMerged());
144 |             
145 |             prData.put("user", pr.getUser().getLogin());
146 |             
147 |             prData.put("base_branch", pr.getBase().getRef());
148 |             prData.put("head_branch", pr.getHead().getRef());
149 |             
150 |             // Get comments
151 |             List<Map<String, Object>> commentsList = new ArrayList<>();
152 |             for (GHIssueComment comment : pr.getComments()) {
153 |                 Map<String, Object> commentData = new HashMap<>();
154 |                 commentData.put("id", comment.getId());
155 |                 commentData.put("user", comment.getUser().getLogin());
156 |                 commentData.put("body", comment.getBody());
157 |                 commentData.put("created_at", comment.getCreatedAt().toString());
158 |                 commentData.put("updated_at", comment.getUpdatedAt().toString());
159 |                 
160 |                 commentsList.add(commentData);
161 |             }
162 |             
163 |             prData.put("comments", commentsList);
164 |             
165 |             // Get files
166 |             List<Map<String, Object>> filesList = new ArrayList<>();
167 |             for (GHPullRequestFileDetail file : pr.listFiles()) {
168 |                 Map<String, Object> fileData = new HashMap<>();
169 |                 fileData.put("filename", file.getFilename());
170 |                 fileData.put("status", file.getStatus());
171 |                 fileData.put("additions", file.getAdditions());
172 |                 fileData.put("deletions", file.getDeletions());
173 |                 fileData.put("changes", file.getChanges());
174 |                 
175 |                 filesList.add(fileData);
176 |             }
177 |             
178 |             prData.put("files", filesList);
179 |             
180 |             result.put("pull_request", prData);
181 |             
182 |             return successMessage(result);
183 |             
184 |         } catch (GHFileNotFoundException e) {
185 |             return errorMessage("Pull request or repository not found: " + e.getMessage());
186 |         } catch (IOException e) {
187 |             return errorMessage("IO error: " + e.getMessage());
188 |         } catch (Exception e) {
189 |             return errorMessage("Unexpected error: " + e.getMessage());
190 |         }
191 |     }
192 |     
193 |     @Tool(description = """
194 |         Create a comment on a pull request.
195 |         Posts a new comment on the specified pull request.
196 |     """)
197 |     public String createPullRequestComment(
198 |             @ToolParam(description = "Repository name in format 'owner/repo'", required = false) String repository,
199 |             @ToolParam(description = "Pull request number") Integer prNumber,
200 |             @ToolParam(description = "Comment text") String body
201 |     ) {
202 |         Map<String, Object> result = new HashMap<>();
203 |         
204 |         try {
205 |             if (prNumber == null) {
206 |                 return errorMessage("Pull request number is required");
207 |             }
208 |             
209 |             if (body == null || body.isEmpty()) {
210 |                 return errorMessage("Comment body is required");
211 |             }
212 |             
213 |             Optional<GitHubEnv> env = GitHubClientFactory.getEnvironment();
214 |             if (env.isEmpty()) {
215 |                 return errorMessage("GitHub is not configured correctly");
216 |             }
217 |             
218 |             GitHubEnv githubEnv = env.get();
219 |             GitHub github = GitHubClientFactory.createClient(githubEnv);
220 |             
221 |             // Use provided repository or default from environment
222 |             String repoName = (repository != null && !repository.isEmpty()) ? 
223 |                                 repository : githubEnv.getFullRepository();
224 |                                 
225 |             if (repoName == null || repoName.isEmpty()) {
226 |                 return errorMessage("Repository name is required");
227 |             }
228 |             
229 |             GHRepository repo = github.getRepository(repoName);
230 |             GHPullRequest pr = repo.getPullRequest(prNumber);
231 |             
232 |             GHIssueComment comment = pr.comment(body);
233 |             
234 |             Map<String, Object> commentData = new HashMap<>();
235 |             commentData.put("id", comment.getId());
236 |             commentData.put("body", comment.getBody());
237 |             commentData.put("html_url", comment.getHtmlUrl().toString());
238 |             commentData.put("created_at", comment.getCreatedAt().toString());
239 |             
240 |             result.put("comment", commentData);
241 |             
242 |             return successMessage(result);
243 |             
244 |         } catch (GHFileNotFoundException e) {
245 |             return errorMessage("Pull request or repository not found: " + e.getMessage());
246 |         } catch (IOException e) {
247 |             return errorMessage("IO error: " + e.getMessage());
248 |         } catch (Exception e) {
249 |             return errorMessage("Unexpected error: " + e.getMessage());
250 |         }
251 |     }
252 |     
253 |     @Tool(description = """
254 |         Merge a pull request.
255 |         Merges the pull request with the specified merge method.
256 |     """)
257 |     public String mergePullRequest(
258 |             @ToolParam(description = "Repository name in format 'owner/repo'", required = false) String repository,
259 |             @ToolParam(description = "Pull request number") Integer prNumber,
260 |             @ToolParam(description = "Commit message for the merge", required = false) String commitMessage,
261 |             @ToolParam(description = "Merge method (merge, squash, rebase)", required = false) String mergeMethod
262 |     ) {
263 |         Map<String, Object> result = new HashMap<>();
264 |         
265 |         try {
266 |             if (prNumber == null) {
267 |                 return errorMessage("Pull request number is required");
268 |             }
269 |             
270 |             Optional<GitHubEnv> env = GitHubClientFactory.getEnvironment();
271 |             if (env.isEmpty()) {
272 |                 return errorMessage("GitHub is not configured correctly");
273 |             }
274 |             
275 |             GitHubEnv githubEnv = env.get();
276 |             GitHub github = GitHubClientFactory.createClient(githubEnv);
277 |             
278 |             // Use provided repository or default from environment
279 |             String repoName = (repository != null && !repository.isEmpty()) ? 
280 |                                 repository : githubEnv.getFullRepository();
281 |                                 
282 |             if (repoName == null || repoName.isEmpty()) {
283 |                 return errorMessage("Repository name is required");
284 |             }
285 |             
286 |             GHRepository repo = github.getRepository(repoName);
287 |             GHPullRequest pr = repo.getPullRequest(prNumber);
288 |             
289 |             // Check if PR is already merged
290 |             if (pr.isMerged()) {
291 |                 return errorMessage("Pull request is already merged");
292 |             }
293 |             
294 |             // Set default merge method if not provided
295 |             String method = (mergeMethod != null && !mergeMethod.isEmpty()) ? 
296 |                              mergeMethod.toLowerCase() : "merge";
297 |             
298 |             boolean success = switch (method) {
299 |                 case "squash" -> {
300 |                     pr.merge(commitMessage, null, GHPullRequest.MergeMethod.SQUASH);
301 |                     yield true;
302 |                 }
303 |                 case "rebase" -> {
304 |                     pr.merge(commitMessage, null, GHPullRequest.MergeMethod.REBASE);
305 |                     yield true;
306 |                 }
307 |                 case "merge" -> {
308 |                     pr.merge(commitMessage, null, GHPullRequest.MergeMethod.MERGE);
309 |                     yield true;
310 |                 }
311 |                 default -> false;
312 |             };
313 | 
314 |             if (success) {
315 |                 result.put("merged", true);
316 |                 result.put("method", method);
317 |                 result.put("pull_request_number", prNumber);
318 |                 result.put("repository", repoName);
319 | 
320 |                 return successMessage(result);
321 |             } else {
322 |                 return errorMessage("Failed to merge pull request");
323 |             }
324 | 
325 |         } catch (GHFileNotFoundException e) {
326 |             return errorMessage("Pull request or repository not found: " + e.getMessage());
327 |         } catch (IOException e) {
328 |             return errorMessage("IO error: " + e.getMessage());
329 |         } catch (Exception e) {
330 |             return errorMessage("Unexpected error: " + e.getMessage());
331 |         }
332 |     }
333 | }
334 | 
```
--------------------------------------------------------------------------------
/src/main/java/com/devoxx/agentic/github/tools/ContentService.java:
--------------------------------------------------------------------------------
```java
  1 | package com.devoxx.agentic.github.tools;
  2 | 
  3 | import org.kohsuke.github.*;
  4 | import org.springframework.ai.tool.annotation.Tool;
  5 | import org.springframework.ai.tool.annotation.ToolParam;
  6 | import org.springframework.stereotype.Service;
  7 | 
  8 | import java.io.IOException;
  9 | import java.util.*;
 10 | import java.nio.charset.StandardCharsets;
 11 | import java.util.Base64;
 12 | 
 13 | /**
 14 |  * Service for GitHub repository content management operations
 15 |  */
 16 | @Service
 17 | public class ContentService extends AbstractToolService {
 18 | 
 19 |     @Tool(description = """
 20 |         Get the contents of a file in a repository.
 21 |         Returns the file content and metadata such as size and sha.
 22 |     """)
 23 |     public String getFileContents(
 24 |             @ToolParam(description = "Repository name in format 'owner/repo'", required = false) String repository,
 25 |             @ToolParam(description = "Path to the file in the repository") String path,
 26 |             @ToolParam(description = "Branch or commit SHA (defaults to the default branch)", required = false) String ref
 27 |     ) {
 28 |         Map<String, Object> result = new HashMap<>();
 29 |         
 30 |         try {
 31 |             if (path == null || path.isEmpty()) {
 32 |                 return errorMessage("File path is required");
 33 |             }
 34 |             
 35 |             Optional<GitHubEnv> env = GitHubClientFactory.getEnvironment();
 36 |             if (env.isEmpty()) {
 37 |                 return errorMessage("GitHub is not configured correctly");
 38 |             }
 39 |             
 40 |             GitHubEnv githubEnv = env.get();
 41 |             GitHub github = GitHubClientFactory.createClient(githubEnv);
 42 |             
 43 |             // Use provided repository or default from environment
 44 |             String repoName = (repository != null && !repository.isEmpty()) ? 
 45 |                                 repository : githubEnv.getFullRepository();
 46 |                                 
 47 |             if (repoName == null || repoName.isEmpty()) {
 48 |                 return errorMessage("Repository name is required");
 49 |             }
 50 |             
 51 |             GHRepository repo = github.getRepository(repoName);
 52 |             
 53 |             // Get contents, using ref if provided
 54 |             GHContent content;
 55 |             if (ref != null && !ref.isEmpty()) {
 56 |                 content = repo.getFileContent(path, ref);
 57 |             } else {
 58 |                 content = repo.getFileContent(path);
 59 |             }
 60 |             
 61 |             // Check if it's a file
 62 |             if (content.isDirectory()) {
 63 |                 return errorMessage("Path points to a directory, not a file");
 64 |             }
 65 | 
 66 |             var contentData = getContentDetails(content);
 67 |             contentData.put("type", content.getType());
 68 |             contentData.put("url", content.getHtmlUrl());
 69 |             contentData.put("download_url", content.getDownloadUrl());
 70 |             
 71 |             // Get and decode content 
 72 |             String base64Content = content.getContent();
 73 |             if (base64Content != null) {
 74 |                 // The content is base64 encoded
 75 |                 String decodedContent = new String(Base64.getDecoder().decode(base64Content), StandardCharsets.UTF_8);
 76 |                 contentData.put("content", decodedContent);
 77 |             } else {
 78 |                 contentData.put("content", "");
 79 |             }
 80 |             
 81 |             result.put("file", contentData);
 82 |             
 83 |             return successMessage(result);
 84 |             
 85 |         } catch (GHFileNotFoundException e) {
 86 |             return errorMessage("File or repository not found: " + e.getMessage());
 87 |         } catch (IOException e) {
 88 |             return errorMessage("IO error: " + e.getMessage());
 89 |         } catch (Exception e) {
 90 |             return errorMessage("Unexpected error: " + e.getMessage());
 91 |         }
 92 |     }
 93 | 
 94 |     @Tool(description = """
 95 |     List contents of a directory in a repository.
 96 |     Returns a list of files and directories at the specified path.
 97 |     """)
 98 |     public String listDirectoryContents(
 99 |             @ToolParam(description = "Repository name in format 'owner/repo'", required = false) String repository,
100 |             @ToolParam(description = "Path to the directory in the repository (use '/' for root)", required = false) String path,
101 |             @ToolParam(description = "Branch or commit SHA (defaults to the default branch)", required = false) String ref
102 |     ) {
103 |         Map<String, Object> result = new HashMap<>();
104 |         
105 |         try {
106 |             Optional<GitHubEnv> env = GitHubClientFactory.getEnvironment();
107 |             if (env.isEmpty()) {
108 |                 return errorMessage("GitHub is not configured correctly");
109 |             }
110 |             
111 |             GitHubEnv githubEnv = env.get();
112 |             GitHub github = GitHubClientFactory.createClient(githubEnv);
113 |             
114 |             // Use provided repository or default from environment
115 |             String repoName = (repository != null && !repository.isEmpty()) ? 
116 |                                 repository : githubEnv.getFullRepository();
117 |                                 
118 |             if (repoName == null || repoName.isEmpty()) {
119 |                 return errorMessage("Repository name is required");
120 |             }
121 |             
122 |             GHRepository repo = github.getRepository(repoName);
123 |             
124 |             // Set default path if not provided
125 |             String dirPath = (path != null && !path.isEmpty()) ? path : "";
126 |             
127 |             // Get contents, using ref if provided
128 |             List<GHContent> contents;
129 |             if (ref != null && !ref.isEmpty()) {
130 |                 contents = repo.getDirectoryContent(dirPath, ref);
131 |             } else {
132 |                 contents = repo.getDirectoryContent(dirPath);
133 |             }
134 |             
135 |             List<Map<String, Object>> contentsList = new ArrayList<>();
136 |             
137 |             for (GHContent content : contents) {
138 |                 Map<String, Object> contentData = getContentDetails(content);
139 |                 contentData.put("type", content.isDirectory() ? "directory" : "file");
140 |                 contentData.put("url", content.getHtmlUrl());
141 |                 if (!content.isDirectory()) {
142 |                     contentData.put("download_url", content.getDownloadUrl());
143 |                 }
144 |                 
145 |                 contentsList.add(contentData);
146 |             }
147 |             
148 |             result.put("contents", contentsList);
149 |             result.put("path", dirPath);
150 |             
151 |             return successMessage(result);
152 |             
153 |         } catch (GHFileNotFoundException e) {
154 |             return errorMessage("Directory or repository not found: " + e.getMessage());
155 |         } catch (IOException e) {
156 |             return errorMessage("IO error: " + e.getMessage());
157 |         } catch (Exception e) {
158 |             return errorMessage("Unexpected error: " + e.getMessage());
159 |         }
160 |     }
161 | 
162 |     private Map<String, Object> getContentDetails(GHContent content) {
163 |         Map<String, Object> contentData = new HashMap<>();
164 |         contentData.put("name", content.getName());
165 |         contentData.put("path", content.getPath());
166 |         contentData.put("sha", content.getSha());
167 |         contentData.put("size", content.getSize());
168 |         return contentData;
169 |     }
170 | 
171 |     @Tool(description = """
172 |     Create or update a file in a repository.
173 |     If the file doesn't exist, it will be created. If it exists, it will be updated.
174 |     """)
175 |     public String createOrUpdateFile(
176 |             @ToolParam(description = "Repository name in format 'owner/repo'", required = false) String repository,
177 |             @ToolParam(description = "Path to the file in the repository") String path,
178 |             @ToolParam(description = "File content") String content,
179 |             @ToolParam(description = "Commit message") String message,
180 |             @ToolParam(description = "Branch name (defaults to the default branch)", required = false) String branch,
181 |             @ToolParam(description = "Current file SHA (required for updates, not for new files)", required = false) String sha
182 |     ) {
183 |         Map<String, Object> result = new HashMap<>();
184 |         
185 |         try {
186 |             if (path == null || path.isEmpty()) {
187 |                 return errorMessage("File path is required");
188 |             }
189 |             
190 |             if (content == null) {
191 |                 return errorMessage("File content is required");
192 |             }
193 |             
194 |             if (message == null || message.isEmpty()) {
195 |                 return errorMessage("Commit message is required");
196 |             }
197 |             
198 |             Optional<GitHubEnv> env = GitHubClientFactory.getEnvironment();
199 |             if (env.isEmpty()) {
200 |                 return errorMessage("GitHub is not configured correctly");
201 |             }
202 |             
203 |             GitHubEnv githubEnv = env.get();
204 |             GitHub github = GitHubClientFactory.createClient(githubEnv);
205 |             
206 |             // Use provided repository or default from environment
207 |             String repoName = (repository != null && !repository.isEmpty()) ? 
208 |                                 repository : githubEnv.getFullRepository();
209 |                                 
210 |             if (repoName == null || repoName.isEmpty()) {
211 |                 return errorMessage("Repository name is required");
212 |             }
213 |             
214 |             GHRepository repo = github.getRepository(repoName);
215 |             
216 |             // Determine branch to use
217 |             String branchToUse = (branch != null && !branch.isEmpty()) ? 
218 |                                   branch : repo.getDefaultBranch();
219 |             
220 |             // Create GHContentBuilder
221 |             GHContentBuilder contentBuilder = repo.createContent()
222 |                     .content(content)
223 |                     .message(message)
224 |                     .path(path)
225 |                     .branch(branchToUse);
226 |             
227 |             // Add SHA if updating an existing file
228 |             if (sha != null && !sha.isEmpty()) {
229 |                 contentBuilder.sha(sha);
230 |             }
231 |             
232 |             // Commit the changes
233 |             GHContentUpdateResponse response = contentBuilder.commit();
234 |             
235 |             // Prepare response data
236 |             Map<String, Object> contentData = new HashMap<>();
237 |             contentData.put("path", path);
238 |             
239 |             // Get the commit info
240 |             GitCommit commit = response.getCommit();
241 |             Map<String, Object> commitData = new HashMap<>();
242 |             commitData.put("sha", commit.getSHA1());
243 |             commitData.put("url", commit.getHtmlUrl());
244 |             commitData.put("message", commit.getMessage());
245 |             contentData.put("commit", commitData);
246 |             
247 |             // Get the content info
248 |             GHContent fileContent = response.getContent();
249 |             contentData.put("sha", fileContent.getSha());
250 |             contentData.put("name", fileContent.getName());
251 |             contentData.put("url", fileContent.getHtmlUrl());
252 |             
253 |             result.put("operation", sha != null ? "update" : "create");
254 |             result.put("file", contentData);
255 |             
256 |             return successMessage(result);
257 |             
258 |         } catch (GHFileNotFoundException e) {
259 |             return errorMessage("Repository not found: " + e.getMessage());
260 |         } catch (IOException e) {
261 |             return errorMessage("IO error: " + e.getMessage());
262 |         } catch (Exception e) {
263 |             return errorMessage("Unexpected error: " + e.getMessage());
264 |         }
265 |     }
266 | 
267 |     @Tool(description = """
268 |     Search for code within repositories.
269 |     Searches GitHub for code matching the query.
270 |     """)
271 |     public String searchCode(
272 |             @ToolParam(description = "Search query") String query,
273 |             @ToolParam(description = "Repository name in format 'owner/repo' to limit search", required = false) String repository,
274 |             @ToolParam(description = "Filter by file extension (e.g., 'java', 'py')", required = false) String extension,
275 |             @ToolParam(description = "Maximum number of results to return", required = false) Integer limit
276 |     ) {
277 |         Map<String, Object> result = new HashMap<>();
278 |         
279 |         try {
280 |             if (query == null || query.isEmpty()) {
281 |                 return errorMessage("Search query is required");
282 |             }
283 |             
284 |             Optional<GitHubEnv> env = GitHubClientFactory.getEnvironment();
285 |             if (env.isEmpty()) {
286 |                 return errorMessage("GitHub is not configured correctly");
287 |             }
288 |             
289 |             GitHubEnv githubEnv = env.get();
290 |             GitHub github = GitHubClientFactory.createClient(githubEnv);
291 |             
292 |             // Build search query
293 |             StringBuilder queryBuilder = new StringBuilder(query);
294 |             
295 |             // Add repository filter if provided
296 |             if (repository != null && !repository.isEmpty()) {
297 |                 queryBuilder.append(" repo:").append(repository);
298 |             }
299 |             
300 |             // Add extension filter if provided
301 |             if (extension != null && !extension.isEmpty()) {
302 |                 queryBuilder.append(" extension:").append(extension);
303 |             }
304 |             
305 |             int actualLimit = (limit != null && limit > 0) ? limit : 20; // Default to 20 results
306 |             
307 |             GHContentSearchBuilder searchBuilder = github.searchContent()
308 |                     .q(queryBuilder.toString());
309 |             
310 |             List<Map<String, Object>> resultsList = new ArrayList<>();
311 |             int count = 0;
312 |             
313 |             for (GHContent content : searchBuilder.list().withPageSize(actualLimit)) {
314 |                 if (count >= actualLimit) {
315 |                     break;
316 |                 }
317 |                 
318 |                 Map<String, Object> contentData = new HashMap<>();
319 |                 contentData.put("name", content.getName());
320 |                 contentData.put("path", content.getPath());
321 |                 contentData.put("sha", content.getSha());
322 |                 contentData.put("repository", content.getOwner().getFullName());
323 |                 contentData.put("html_url", content.getHtmlUrl());
324 |                 
325 |                 // Try to get a snippet of content for context
326 |                 try {
327 |                     // The content is base64 encoded
328 |                     String base64Content = content.getContent();
329 |                     if (base64Content != null) {
330 |                         String decodedContent = new String(Base64.getDecoder().decode(base64Content), StandardCharsets.UTF_8);
331 |                         
332 |                         // Get a snippet (first 200 chars or less)
333 |                         int snippetLength = Math.min(decodedContent.length(), 200);
334 |                         String snippet = decodedContent.substring(0, snippetLength);
335 |                         if (snippetLength < decodedContent.length()) {
336 |                             snippet += "...";
337 |                         }
338 |                         
339 |                         contentData.put("text_matches", snippet);
340 |                     }
341 |                 } catch (Exception e) {
342 |                     // Ignore content retrieval errors for search results
343 |                     contentData.put("text_matches", "[Content unavailable]");
344 |                 }
345 |                 
346 |                 resultsList.add(contentData);
347 |                 count++;
348 |             }
349 |             
350 |             result.put("items", resultsList);
351 |             result.put("count", resultsList.size());
352 |             result.put("query", queryBuilder.toString());
353 |             
354 |             return successMessage(result);
355 |             
356 |         } catch (IOException e) {
357 |             return errorMessage("IO error: " + e.getMessage());
358 |         } catch (Exception e) {
359 |             return errorMessage("Unexpected error: " + e.getMessage());
360 |         }
361 |     }
362 | }
363 | 
```
--------------------------------------------------------------------------------
/src/main/java/com/devoxx/agentic/github/tools/IssueService.java:
--------------------------------------------------------------------------------
```java
  1 | package com.devoxx.agentic.github.tools;
  2 | 
  3 | import org.kohsuke.github.*;
  4 | import org.springframework.ai.tool.annotation.Tool;
  5 | import org.springframework.ai.tool.annotation.ToolParam;
  6 | import org.springframework.stereotype.Service;
  7 | 
  8 | import java.io.IOException;
  9 | import java.util.*;
 10 | 
 11 | /**
 12 |  * Service for GitHub issue-related operations
 13 |  */
 14 | @Service
 15 | public class IssueService extends AbstractToolService {
 16 | 
 17 |     @Tool(description = """
 18 |         List issues for a repository.
 19 |         Returns issues with filtering options for state, labels, and more.
 20 |     """)
 21 |     public String listIssues(
 22 |             @ToolParam(description = "Repository name in format 'owner/repo'", required = false) String repository,
 23 |             @ToolParam(description = "State of issues to return (open, closed, all)", required = false) String state,
 24 |             @ToolParam(description = "Maximum number of issues to return", required = false) Integer limit
 25 |     ) {
 26 |         Map<String, Object> result = new HashMap<>();
 27 |         
 28 |         try {
 29 |             Optional<GitHubEnv> env = GitHubClientFactory.getEnvironment();
 30 |             if (env.isEmpty()) {
 31 |                 return errorMessage("GitHub is not configured correctly");
 32 |             }
 33 |             
 34 |             GitHubEnv githubEnv = env.get();
 35 |             GitHub github = GitHubClientFactory.createClient(githubEnv);
 36 |             
 37 |             // Use provided repository or default from environment
 38 |             String repoName = (repository != null && !repository.isEmpty()) ? 
 39 |                                 repository : githubEnv.getFullRepository();
 40 |                                 
 41 |             if (repoName == null || repoName.isEmpty()) {
 42 |                 return errorMessage("Repository name is required");
 43 |             }
 44 |             
 45 |             GHRepository repo = github.getRepository(repoName);
 46 |             
 47 |             GHIssueState issueState = GHIssueState.OPEN;
 48 |             if (state != null) {
 49 |                 issueState = switch (state.toLowerCase()) {
 50 |                     case "closed" -> GHIssueState.CLOSED;
 51 |                     case "all" -> GHIssueState.ALL;
 52 |                     default -> issueState;
 53 |                 };
 54 |             }
 55 |             
 56 |             List<GHIssue> issues;
 57 |             
 58 | //            if (labels != null && !labels.isEmpty()) {
 59 | //                String[] labelArray = labels.split(",");
 60 | //                issues = repo.getIssues(issueState, labelArray);
 61 | //            } else {
 62 |                 issues = repo.getIssues(issueState);
 63 | //            }
 64 |             
 65 |             List<Map<String, Object>> issueList = new ArrayList<>();
 66 |             int count = 0;
 67 |             
 68 |             for (GHIssue issue : issues) {
 69 |                 if (limit != null && count >= limit) {
 70 |                     break;
 71 |                 }
 72 |                 
 73 |                 Map<String, Object> issueData = new HashMap<>();
 74 |                 issueData.put("number", issue.getNumber());
 75 |                 issueData.put("title", issue.getTitle());
 76 |                 issueData.put("state", issue.getState().name().toLowerCase());
 77 |                 issueData.put("html_url", issue.getHtmlUrl().toString());
 78 |                 
 79 |                 List<String> issueLabels = new ArrayList<>();
 80 |                 for (GHLabel label : issue.getLabels()) {
 81 |                     issueLabels.add(label.getName());
 82 |                 }
 83 |                 issueData.put("labels", issueLabels);
 84 |                 
 85 |                 issueData.put("created_at", issue.getCreatedAt().toString());
 86 |                 issueData.put("updated_at", issue.getUpdatedAt().toString());
 87 |                 issueData.put("closed_at", issue.getClosedAt() != null ? issue.getClosedAt().toString() : null);
 88 |                 
 89 |                 if (issue.getAssignee() != null) {
 90 |                     issueData.put("assignee", issue.getAssignee().getLogin());
 91 |                 }
 92 |                 
 93 |                 issueList.add(issueData);
 94 |                 count++;
 95 |             }
 96 |             
 97 |             result.put("issues", issueList);
 98 |             result.put("total_count", issues.size());
 99 |             
100 |             return successMessage(result);
101 |             
102 |         } catch (GHFileNotFoundException e) {
103 |             return errorMessage("Repository not found: " + e.getMessage());
104 |         } catch (IOException e) {
105 |             return errorMessage("IO error: " + e.getMessage());
106 |         } catch (Exception e) {
107 |             return errorMessage("Unexpected error: " + e.getMessage());
108 |         }
109 |     }
110 | 
111 |     @Tool(description = """
112 |         Get a specific issue in a repository.
113 |         Returns detailed information about the issue.
114 |     """)
115 |     public String getIssue(
116 |             @ToolParam(description = "Repository name in format 'owner/repo'", required = false) String repository,
117 |             @ToolParam(description = "Issue number") Integer issueNumber
118 |     ) {
119 |         Map<String, Object> result = new HashMap<>();
120 |         
121 |         try {
122 |             if (issueNumber == null) {
123 |                 return errorMessage("Issue number is required");
124 |             }
125 |             
126 |             Optional<GitHubEnv> env = GitHubClientFactory.getEnvironment();
127 |             if (env.isEmpty()) {
128 |                 return errorMessage("GitHub is not configured correctly");
129 |             }
130 |             
131 |             GitHubEnv githubEnv = env.get();
132 |             GitHub github = GitHubClientFactory.createClient(githubEnv);
133 |             
134 |             // Use provided repository or default from environment
135 |             String repoName = (repository != null && !repository.isEmpty()) ? 
136 |                                 repository : githubEnv.getFullRepository();
137 |                                 
138 |             if (repoName == null || repoName.isEmpty()) {
139 |                 return errorMessage("Repository name is required");
140 |             }
141 |             
142 |             GHRepository repo = github.getRepository(repoName);
143 |             GHIssue issue = repo.getIssue(issueNumber);
144 |             
145 |             Map<String, Object> issueData = new HashMap<>();
146 |             issueData.put("number", issue.getNumber());
147 |             issueData.put("title", issue.getTitle());
148 |             issueData.put("body", issue.getBody());
149 |             issueData.put("state", issue.getState().name().toLowerCase());
150 |             issueData.put("html_url", issue.getHtmlUrl().toString());
151 |             
152 |             List<String> labels = new ArrayList<>();
153 |             for (GHLabel label : issue.getLabels()) {
154 |                 labels.add(label.getName());
155 |             }
156 |             issueData.put("labels", labels);
157 |             
158 |             issueData.put("created_at", issue.getCreatedAt().toString());
159 |             issueData.put("updated_at", issue.getUpdatedAt().toString());
160 |             issueData.put("closed_at", issue.getClosedAt() != null ? issue.getClosedAt().toString() : null);
161 |             
162 |             if (issue.getAssignee() != null) {
163 |                 issueData.put("assignee", issue.getAssignee().getLogin());
164 |             }
165 |             
166 |             // Get comments
167 |             List<Map<String, Object>> commentsList = new ArrayList<>();
168 |             for (GHIssueComment comment : issue.getComments()) {
169 |                 Map<String, Object> commentData = new HashMap<>();
170 |                 commentData.put("id", comment.getId());
171 |                 commentData.put("user", comment.getUser().getLogin());
172 |                 commentData.put("body", comment.getBody());
173 |                 commentData.put("created_at", comment.getCreatedAt().toString());
174 |                 commentData.put("updated_at", comment.getUpdatedAt().toString());
175 |                 
176 |                 commentsList.add(commentData);
177 |             }
178 |             
179 |             issueData.put("comments", commentsList);
180 |             issueData.put("comments_count", commentsList.size());
181 |             
182 |             result.put("issue", issueData);
183 |             
184 |             return successMessage(result);
185 |             
186 |         } catch (GHFileNotFoundException e) {
187 |             return errorMessage("Issue or repository not found: " + e.getMessage());
188 |         } catch (IOException e) {
189 |             return errorMessage("IO error: " + e.getMessage());
190 |         } catch (Exception e) {
191 |             return errorMessage("Unexpected error: " + e.getMessage());
192 |         }
193 |     }
194 | 
195 |     @Tool(description = """
196 |     Create a new issue in a repository.
197 |     Creates an issue with the specified title, body, and optional labels.
198 |     """)
199 |     public String createIssue(
200 |             @ToolParam(description = "Repository name in format 'owner/repo'", required = false) String repository,
201 |             @ToolParam(description = "Issue title") String title,
202 |             @ToolParam(description = "Issue body/description", required = false) String body,
203 |             @ToolParam(description = "Comma-separated list of labels", required = false) String labels
204 |     ) {
205 |         Map<String, Object> result = new HashMap<>();
206 |         
207 |         try {
208 |             if (title == null || title.isEmpty()) {
209 |                 return errorMessage("Issue title is required");
210 |             }
211 |             
212 |             Optional<GitHubEnv> env = GitHubClientFactory.getEnvironment();
213 |             if (env.isEmpty()) {
214 |                 return errorMessage("GitHub is not configured correctly");
215 |             }
216 |             
217 |             GitHubEnv githubEnv = env.get();
218 |             GitHub github = GitHubClientFactory.createClient(githubEnv);
219 |             
220 |             // Use provided repository or default from environment
221 |             String repoName = (repository != null && !repository.isEmpty()) ? 
222 |                                 repository : githubEnv.getFullRepository();
223 |                                 
224 |             if (repoName == null || repoName.isEmpty()) {
225 |                 return errorMessage("Repository name is required");
226 |             }
227 |             
228 |             GHRepository repo = github.getRepository(repoName);
229 |             
230 |             GHIssueBuilder issueBuilder = repo.createIssue(title);
231 |             
232 |             if (body != null && !body.isEmpty()) {
233 |                 issueBuilder.body(body);
234 |             }
235 |             
236 |             if (labels != null && !labels.isEmpty()) {
237 |                 String[] labelArray = labels.split(",");
238 |                 for (String label : labelArray) {
239 |                     issueBuilder.label(label.trim());
240 |                 }
241 |             }
242 |             
243 |             GHIssue issue = issueBuilder.create();
244 |             
245 |             Map<String, Object> issueData = new HashMap<>();
246 |             issueData.put("number", issue.getNumber());
247 |             issueData.put("title", issue.getTitle());
248 |             issueData.put("body", issue.getBody());
249 |             issueData.put("html_url", issue.getHtmlUrl().toString());
250 |             
251 |             result.put("issue", issueData);
252 |             
253 |             return successMessage(result);
254 |             
255 |         } catch (GHFileNotFoundException e) {
256 |             return errorMessage("Repository not found: " + e.getMessage());
257 |         } catch (IOException e) {
258 |             return errorMessage("IO error: " + e.getMessage());
259 |         } catch (Exception e) {
260 |             return errorMessage("Unexpected error: " + e.getMessage());
261 |         }
262 |     }
263 | 
264 |     @Tool(description = """
265 |     Add a comment to an issue.
266 |     Posts a new comment on the specified issue.
267 |     """)
268 |     public String addIssueComment(
269 |             @ToolParam(description = "Repository name in format 'owner/repo'", required = false) String repository,
270 |             @ToolParam(description = "Issue number") Integer issueNumber,
271 |             @ToolParam(description = "Comment text") String body
272 |     ) {
273 |         Map<String, Object> result = new HashMap<>();
274 |         
275 |         try {
276 |             if (issueNumber == null) {
277 |                 return errorMessage("Issue number is required");
278 |             }
279 |             
280 |             if (body == null || body.isEmpty()) {
281 |                 return errorMessage("Comment body is required");
282 |             }
283 |             
284 |             Optional<GitHubEnv> env = GitHubClientFactory.getEnvironment();
285 |             if (env.isEmpty()) {
286 |                 return errorMessage("GitHub is not configured correctly");
287 |             }
288 |             
289 |             GitHubEnv githubEnv = env.get();
290 |             GitHub github = GitHubClientFactory.createClient(githubEnv);
291 |             
292 |             // Use provided repository or default from environment
293 |             String repoName = (repository != null && !repository.isEmpty()) ? 
294 |                                 repository : githubEnv.getFullRepository();
295 |                                 
296 |             if (repoName == null || repoName.isEmpty()) {
297 |                 return errorMessage("Repository name is required");
298 |             }
299 |             
300 |             GHRepository repo = github.getRepository(repoName);
301 |             GHIssue issue = repo.getIssue(issueNumber);
302 |             
303 |             GHIssueComment comment = issue.comment(body);
304 |             
305 |             Map<String, Object> commentData = new HashMap<>();
306 |             commentData.put("id", comment.getId());
307 |             commentData.put("body", comment.getBody());
308 |             commentData.put("html_url", comment.getHtmlUrl().toString());
309 |             commentData.put("created_at", comment.getCreatedAt().toString());
310 |             
311 |             result.put("comment", commentData);
312 |             
313 |             return successMessage(result);
314 |             
315 |         } catch (GHFileNotFoundException e) {
316 |             return errorMessage("Issue or repository not found: " + e.getMessage());
317 |         } catch (IOException e) {
318 |             return errorMessage("IO error: " + e.getMessage());
319 |         } catch (Exception e) {
320 |             return errorMessage("Unexpected error: " + e.getMessage());
321 |         }
322 |     }
323 |     
324 |     @Tool(description = """
325 |     Search for issues.
326 |     Searches for issues matching the query across GitHub or in a specific repository.
327 |     """)
328 |     public String searchIssues(
329 |             @ToolParam(description = "Search query") String query,
330 |             @ToolParam(description = "Repository name in format 'owner/repo' to limit search", required = false) String repository,
331 |             @ToolParam(description = "State of issues to search for (open, closed)", required = false) String state,
332 |             @ToolParam(description = "Maximum number of results to return", required = false) Integer limit
333 |     ) {
334 |         Map<String, Object> result = new HashMap<>();
335 |         
336 |         try {
337 |             if (query == null || query.isEmpty()) {
338 |                 return errorMessage("Search query is required");
339 |             }
340 |             
341 |             Optional<GitHubEnv> env = GitHubClientFactory.getEnvironment();
342 |             if (env.isEmpty()) {
343 |                 return errorMessage("GitHub is not configured correctly");
344 |             }
345 |             
346 |             GitHubEnv githubEnv = env.get();
347 |             GitHub github = GitHubClientFactory.createClient(githubEnv);
348 |             
349 |             // Build search query
350 |             StringBuilder queryBuilder = new StringBuilder(query);
351 |             
352 |             // Add repository filter if provided
353 |             if (repository != null && !repository.isEmpty()) {
354 |                 queryBuilder.append(" repo:").append(repository);
355 |             }
356 |             
357 |             // Add state filter if provided
358 |             if (state != null && !state.isEmpty()) {
359 |                 queryBuilder.append(" is:").append(state);
360 |             }
361 |             
362 |             int actualLimit = (limit != null && limit > 0) ? limit : 10;
363 |             
364 |             GHIssueSearchBuilder searchBuilder = github.searchIssues()
365 |                     .q(queryBuilder.toString())
366 |                     .order(GHDirection.DESC)
367 |                     .sort(GHIssueSearchBuilder.Sort.CREATED);
368 |             
369 |             List<Map<String, Object>> issueList = new ArrayList<>();
370 |             
371 |             searchBuilder.list().withPageSize(actualLimit).iterator().forEachRemaining(issue -> {
372 |                 if (issueList.size() < actualLimit) {
373 |                     Map<String, Object> issueData = new HashMap<>();
374 |                     issueData.put("number", issue.getNumber());
375 |                     issueData.put("title", issue.getTitle());
376 |                     issueData.put("state", issue.getState().name().toLowerCase());
377 |                     issueData.put("repository", issue.getRepository().getFullName());
378 |                     issueData.put("html_url", issue.getHtmlUrl().toString());
379 |                     try {
380 |                         issueData.put("created_at", issue.getCreatedAt().toString());
381 |                     } catch (IOException e) {
382 |                         throw new RuntimeException(e);
383 |                     }
384 | 
385 |                     issueList.add(issueData);
386 |                 }
387 |             });
388 |             
389 |             result.put("issues", issueList);
390 |             result.put("query", queryBuilder.toString());
391 |             
392 |             return successMessage(result);
393 |             
394 |         } catch (IOException e) {
395 |             return errorMessage("IO error: " + e.getMessage());
396 |         } catch (Exception e) {
397 |             return errorMessage("Unexpected error: " + e.getMessage());
398 |         }
399 |     }
400 | }
401 | 
```