Repository: github/github-mcp-server

Issue:
Unable to download the docker image
I setup the MCP server as described in the `README`. But starting the server I get following errors:

```bash
[info] Starting server github
[info] Connection state: Starting
[info] Starting server from LocalProcess extension host
[info] Connection state: Starting
[info] Connection state: Running
[warning] [server stderr] Unable to find image 'ghcr.io/github/github-mcp-server:latest' locally
[warning] [server stderr] docker: Error response from daemon: Head "https://ghcr.io/v2/github/github-mcp-server/manifests/latest": denied: denied
[warning] [server stderr] 
[warning] [server stderr] Run 'docker run --help' for more information
[info] Connection state: Error Process exited with code 125
```

Any idea what I am doing wrong?

Hints from issue comments:
@edgeq: ➕ - also unable to pull the docker image. Even when pulling manually: 

```
docker pull ghcr.io/github/github-mcp-server
Using default tag: latest
Error response from daemon: Head "https://ghcr.io/v2/github/github-mcp-server/manifests/latest": denied: denied
```@edgeq: My current workaround is to use the official [anthropic mcp server](https://github.com/modelcontextprotocol/servers/tree/main/src/github). It's in deprecation, but it's a trusted source and still works.   @juruen: @akordowski @edgeq is there any chance that you were logged into the gh docker registry with an expired token?

I'm unable to reproduce this as I can successfully pull the image anonymously:

```
juruen@moonbase:~$ docker logout ghcr.io  # logout from gh docker registry
Removing login credentials for ghcr.io
juruen@moonbase:~$ docker pull ghcr.io/github/github-mcp-server
Using default tag: latest
latest: Pulling from github/github-mcp-server
3d78e577de35: Pull complete
bfb59b82a9b6: Pull complete
4eff9a62d888: Pull complete
a62778643d56: Pull complete
7c12895b777b: Pull complete
3214acf345c0: Pull complete
5664b15f108b: Pull complete
0bab15eea81d: Pull complete
4aa0ea1413d3: Pull complete
da7816fa955e: Pull complete
9aee425378d2: Pull complete
d00c3209d929: Pull complete
221438ca359c: Pull complete
e76e88e5b481: Pull complete
4d3ee8eeb83f: Pull complete
Digest: sha256:faa1dfed068fb1d9c866a5ce4909947a5f2a5a72130815fba56c32bb441ca83f
Status: Downloaded newer image for ghcr.io/github/github-mcp-server:latest
ghcr.io/github/github-mcp-server:latest
```@akordowski: @juruen No, for the test I used a fresh created token. Are there maybe special rights that must be set for the token to access the registry?@juruen: > [@juruen](https://github.com/juruen) No, for the test I used a fresh created token. Are there maybe special rights that must be set for the token to access the registry?

You should be able to pull the docker image anonymously, in the example I posted above, I do `docker logout ghcr.io` to make sure that I'm not logged in.

Any chance you could try that?@akordowski: > You should be able to pull the docker image anonymously, in the example I posted above, I do `docker logout ghcr.io` to make sure that I'm not logged in.

The `docker logout ghcr.io` command helped and I was able to pull the image. I wonder why this was an issue, as I was not logged in into `ghcr.io` at all. At least at works. Thank you very much!

A note in the `README` regarding this issue might be helpful for other users.

Solution patch:
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 954bc41..0481233 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -1 +1 @@
-* @juruen @sammorrowdrums @williammartin @toby
+* @github/github-mcp-server
diff --git a/.gitignore b/.gitignore
index 9fb1dca..1264936 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,13 @@
 .idea
 cmd/github-mcp-server/github-mcp-server
+
+# VSCode
+.vscode/*
+!.vscode/launch.json
+
 # Added by goreleaser init:
 dist/
-__debug_bin*
\ No newline at end of file
+__debug_bin*
+
+# Go
+vendor
diff --git a/Dockerfile b/Dockerfile
index 05fe1dd..22c405c 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,6 +1,6 @@
 ARG VERSION="dev"
 
-FROM golang:1.23.7 AS build
+FROM golang:1.24.2 AS build
 # allow this step access to build arg
 ARG VERSION
 # Set the working directory
diff --git a/README.md b/README.md
index 6bfc6ab..eacaef2 100644
--- a/README.md
+++ b/README.md
@@ -15,24 +15,18 @@ automation and interaction capabilities for developers and tools.
 ## Prerequisites
 
 1. To run the server in a container, you will need to have [Docker](https://www.docker.com/) installed.
-2. Once Docker is installed, you will also need to ensure Docker is running.
+2. Once Docker is installed, you will also need to ensure Docker is running. The image is public; if you get errors on pull, you may have an expired token and need to `docker logout ghcr.io`.
 3. Lastly you will need to [Create a GitHub Personal Access Token](https://github.com/settings/personal-access-tokens/new).
 The MCP server can use many of the GitHub APIs, so enable the permissions that you feel comfortable granting your AI tools (to learn more about access tokens, please check out the [documentation](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens)).
 
-
-
 ## Installation
 
 ### Usage with VS Code
 
-For quick installation, use one of the one-click install buttons at the top of this README.
+For quick installation, use one of the one-click install buttons at the top of this README. Once you complete that flow, toggle Agent mode (located by the Copilot Chat text input) and the server will start.
 
 For manual installation, add the following JSON block to your User Settings (JSON) file in VS Code. You can do this by pressing `Ctrl + Shift + P` and typing `Preferences: Open User Settings (JSON)`.
 
-Optionally, you can add it to a file called `.vscode/mcp.json` in your workspace. This will allow you to share the configuration with others.
-
-> Note that the `mcp` key is not needed in the `.vscode/mcp.json` file.
-
 ```json
 {
   "mcp": {
@@ -64,6 +58,39 @@ Optionally, you can add it to a file called `.vscode/mcp.json` in your workspace
 }
 ```
 
+Optionally, you can add a similar example (i.e. without the mcp key) to a file called `.vscode/mcp.json` in your workspace. This will allow you to share the configuration with others.
+
+
+```json
+{
+  "inputs": [
+    {
+      "type": "promptString",
+      "id": "github_token",
+      "description": "GitHub Personal Access Token",
+      "password": true
+    }
+  ],
+  "servers": {
+    "github": {
+      "command": "docker",
+      "args": [
+        "run",
+        "-i",
+        "--rm",
+        "-e",
+        "GITHUB_PERSONAL_ACCESS_TOKEN",
+        "ghcr.io/github/github-mcp-server"
+      ],
+      "env": {
+        "GITHUB_PERSONAL_ACCESS_TOKEN": "${input:github_token}"
+      }
+    }
+  }
+}
+
+```
+
 More about using MCP server tools in VS Code's [agent mode documentation](https://code.visualstudio.com/docs/copilot/chat/mcp-servers).
 
 ### Usage with Claude Desktop
@@ -110,9 +137,91 @@ If you don't have Docker, you can use `go build` to build the binary in the
 }
 ```
 
+## Tool Configuration
+
+The GitHub MCP Server supports enabling or disabling specific groups of functionalities via the `--toolsets` flag. This allows you to control which GitHub API capabilities are available to your AI tools. Enabling only the toolsets that you need can help the LLM with tool choice and reduce the context size.
+
+### Available Toolsets
+
+The following sets of tools are available (all are on by default):
+
+| Toolset                 | Description                                                   |
+| ----------------------- | ------------------------------------------------------------- |
+| `repos`                 | Repository-related tools (file operations, branches, commits) |
+| `issues`                | Issue-related tools (create, read, update, comment)           |
+| `users`                 | Anything relating to GitHub Users                             |
+| `pull_requests`         | Pull request operations (create, merge, review)               |
+| `code_security`         | Code scanning alerts and security features                    |
+| `experiments`           | Experimental features (not considered stable)                 |
+
+#### Specifying Toolsets
+
+To specify toolsets you want available to the LLM, you can pass an allow-list in two ways:
+
+1. **Using Command Line Argument**:
+
+   ```bash
+   github-mcp-server --toolsets repos,issues,pull_requests,code_security
+   ```
+
+2. **Using Environment Variable**:
+   ```bash
+   GITHUB_TOOLSETS="repos,issues,pull_requests,code_security" ./github-mcp-server
+   ```
+
+The environment variable `GITHUB_TOOLSETS` takes precedence over the command line argument if both are provided.
+
+### Using Toolsets With Docker
+
+When using Docker, you can pass the toolsets as environment variables:
+
+```bash
+docker run -i --rm \
+  -e GITHUB_PERSONAL_ACCESS_TOKEN=<your-token> \
+  -e GITHUB_TOOLSETS="repos,issues,pull_requests,code_security,experiments" \
+  ghcr.io/github/github-mcp-server
+```
+
+### The "all" Toolset
+
+The special toolset `all` can be provided to enable all available toolsets regardless of any other configuration:
+
+```bash
+./github-mcp-server --toolsets all
+```
+
+Or using the environment variable:
+
+```bash
+GITHUB_TOOLSETS="all" ./github-mcp-server
+```
+
+## Dynamic Tool Discovery
+
+**Note**: This feature is currently in beta and may not be available in all environments. Please test it out and let us know if you encounter any issues.
+
+Instead of starting with all tools enabled, you can turn on dynamic toolset discovery. Dynamic toolsets allow the MCP host to list and enable toolsets in response to a user prompt. This should help to avoid situations where the model gets confused by the shear number of tools available.
+
+### Using Dynamic Tool Discovery
+
+When using the binary, you can pass the `--dynamic-toolsets` flag.
+
+```bash
+./github-mcp-server --dynamic-toolsets
+```
+
+When using Docker, you can pass the toolsets as environment variables:
+
+```bash
+docker run -i --rm \
+  -e GITHUB_PERSONAL_ACCESS_TOKEN=<your-token> \
+  -e GITHUB_DYNAMIC_TOOLSETS=1 \
+  ghcr.io/github/github-mcp-server
+```
+
 ## GitHub Enterprise Server
 
-The flag `--gh-host` and the environment variable `GH_HOST` can be used to set
+The flag `--gh-host` and the environment variable `GITHUB_HOST` can be used to set
 the GitHub Enterprise Server hostname.
 
 ## i18n / Overriding Descriptions
@@ -331,7 +440,6 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description
 ### Repositories
 
 - **create_or_update_file** - Create or update a single file in a repository
-
   - `owner`: Repository owner (string, required)
   - `repo`: Repository name (string, required)
   - `path`: File path (string, required)
@@ -341,14 +449,12 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description
   - `sha`: File SHA if updating (string, optional)
 
 - **list_branches** - List branches in a GitHub repository
-
   - `owner`: Repository owner (string, required)
   - `repo`: Repository name (string, required)
   - `page`: Page number (number, optional)
   - `perPage`: Results per page (number, optional)
 
 - **push_files** - Push multiple files in a single commit
-
   - `owner`: Repository owner (string, required)
   - `repo`: Repository name (string, required)
   - `branch`: Branch to push to (string, required)
@@ -356,7 +462,6 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description
   - `message`: Commit message (string, required)
 
 - **search_repositories** - Search for GitHub repositories
-
   - `query`: Search query (string, required)
   - `sort`: Sort field (string, optional)
   - `order`: Sort order (string, optional)
@@ -364,27 +469,23 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description
   - `perPage`: Results per page (number, optional)
 
 - **create_repository** - Create a new GitHub repository
-
   - `name`: Repository name (string, required)
   - `description`: Repository description (string, optional)
   - `private`: Whether the repository is private (boolean, optional)
   - `autoInit`: Auto-initialize with README (boolean, optional)
 
 - **get_file_contents** - Get contents of a file or directory
-
   - `owner`: Repository owner (string, required)
   - `repo`: Repository name (string, required)
   - `path`: File path (string, required)
   - `ref`: Git reference (string, optional)
 
 - **fork_repository** - Fork a repository
-
   - `owner`: Repository owner (string, required)
   - `repo`: Repository name (string, required)
   - `organization`: Target organization name (string, optional)
 
 - **create_branch** - Create a new branch
-
   - `owner`: Repository owner (string, required)
   - `repo`: Repository name (string, required)
   - `branch`: New branch name (string, required)
@@ -405,18 +506,17 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description
   - `page`: Page number, for files in the commit (number, optional)
   - `perPage`: Results per page, for files in the commit (number, optional)
 
-### Search
-
 - **search_code** - Search for code across GitHub repositories
-
   - `query`: Search query (string, required)
   - `sort`: Sort field (string, optional)
   - `order`: Sort order (string, optional)
   - `page`: Page number (number, optional)
   - `perPage`: Results per page (number, optional)
 
+### Users
+
 - **search_users** - Search for GitHub users
-  - `query`: Search query (string, required)
+  - `q`: Search query (string, required)
   - `sort`: Sort field (string, optional)
   - `order`: Sort order (string, optional)
   - `page`: Page number (number, optional)
@@ -436,6 +536,22 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description
   - `ref`: Git reference (string, optional)
   - `state`: Alert state (string, optional)
   - `severity`: Alert severity (string, optional)
+  - `tool_name`: The name of the tool used for code scanning (string, optional)
+
+### Secret Scanning
+
+- **get_secret_scanning_alert** - Get a secret scanning alert
+
+  - `owner`: Repository owner (string, required)
+  - `repo`: Repository name (string, required)
+  - `alertNumber`: Alert number (number, required)
+
+- **list_secret_scanning_alerts** - List secret scanning alerts for a repository
+  - `owner`: Repository owner (string, required)
+  - `repo`: Repository name (string, required)
+  - `state`: Alert state (string, optional)
+  - `secret_type`: The secret types to be filtered for in a comma-separated list (string, optional)
+  - `resolution`: The resolution status (string, optional)
 
 ## Resources
 
diff --git a/e2e/README.md b/e2e/README.md
new file mode 100644
index 0000000..21b65bf
--- /dev/null
+++ b/e2e/README.md
@@ -0,0 +1,84 @@
+# End To End (e2e) Tests
+
+The purpose of the E2E tests is to have a simple (currently) test that gives maintainers some confidence in the black box behavior of our artifacts. It does this by:
+ * Building the `github-mcp-server` docker image
+ * Running the image
+ * Interacting with the server via stdio
+ * Issuing requests that interact with the live GitHub API
+
+## Running the Tests
+
+A service must be running that supports image building and container creation via the `docker` CLI.
+
+Since these tests require a token to interact with real resources on the GitHub API, it is gated behind the `e2e` build flag.
+
+```
+GITHUB_MCP_SERVER_E2E_TOKEN=<YOUR TOKEN> go test -v --tags e2e ./e2e
+```
+
+The `GITHUB_MCP_SERVER_E2E_TOKEN` environment variable is mapped to `GITHUB_PERSONAL_ACCESS_TOKEN` internally, but separated to avoid accidental reuse of credentials.
+
+## Example
+
+The following diff adjusts the `get_me` tool to return `foobar` as the user login.
+
+```diff
+diff --git a/pkg/github/context_tools.go b/pkg/github/context_tools.go
+index 1c91d70..ac4ef2b 100644
+--- a/pkg/github/context_tools.go
++++ b/pkg/github/context_tools.go
+@@ -39,6 +39,8 @@ func GetMe(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mc
+                                return mcp.NewToolResultError(fmt.Sprintf("failed to get user: %s", string(body))), nil
+                        }
+
++                       user.Login = sPtr("foobar")
++
+                        r, err := json.Marshal(user)
+                        if err != nil {
+                                return nil, fmt.Errorf("failed to marshal user: %w", err)
+@@ -47,3 +49,7 @@ func GetMe(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mc
+                        return mcp.NewToolResultText(string(r)), nil
+                }
+ }
++
++func sPtr(s string) *string {
++       return &s
++}
+```
+
+Running the tests:
+
+```
+➜ GITHUB_MCP_SERVER_E2E_TOKEN=$(gh auth token) go test -v --tags e2e ./e2e
+=== RUN   TestE2E
+    e2e_test.go:92: Building Docker image for e2e tests...
+    e2e_test.go:36: Starting Stdio MCP client...
+=== RUN   TestE2E/Initialize
+=== RUN   TestE2E/CallTool_get_me
+    e2e_test.go:85:
+                Error Trace:    /Users/williammartin/workspace/github-mcp-server/e2e/e2e_test.go:85
+                Error:          Not equal:
+                                expected: "foobar"
+                                actual  : "williammartin"
+
+                                Diff:
+                                --- Expected
+                                +++ Actual
+                                @@ -1 +1 @@
+                                -foobar
+                                +williammartin
+                Test:           TestE2E/CallTool_get_me
+                Messages:       expected login to match
+--- FAIL: TestE2E (1.05s)
+    --- PASS: TestE2E/Initialize (0.09s)
+    --- FAIL: TestE2E/CallTool_get_me (0.46s)
+FAIL
+FAIL    github.com/github/github-mcp-server/e2e 1.433s
+FAIL
+```
+
+## Limitations
+
+The current test suite is intentionally very limited in scope. This is because the maintenance costs on e2e tests tend to increase significantly over time. To read about some challenges with GitHub integration tests, see [go-github integration tests README](https://github.com/google/go-github/blob/5b75aa86dba5cf4af2923afa0938774f37fa0a67/test/README.md). We will expand this suite circumspectly!
+
+Currently, visibility into failures is not particularly good.
diff --git a/go.mod b/go.mod
index 7c09fba..19716d3 100644
--- a/go.mod
+++ b/go.mod
@@ -3,10 +3,8 @@ module github.com/github/github-mcp-server
 go 1.23.7
 
 require (
-	github.com/docker/docker v28.0.4+incompatible
-	github.com/google/go-cmp v0.7.0
 	github.com/google/go-github/v69 v69.2.0
-	github.com/mark3labs/mcp-go v0.20.1
+	github.com/mark3labs/mcp-go v0.22.0
 	github.com/migueleliasweb/go-github-mock v1.1.0
 	github.com/sirupsen/logrus v1.9.3
 	github.com/spf13/cobra v1.9.1
@@ -15,31 +13,18 @@ require (
 )
 
 require (
-	github.com/Microsoft/go-winio v0.6.2 // indirect
-	github.com/containerd/log v0.1.0 // indirect
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
-	github.com/distribution/reference v0.6.0 // indirect
-	github.com/docker/go-connections v0.5.0 // indirect
-	github.com/docker/go-units v0.5.0 // indirect
-	github.com/felixge/httpsnoop v1.0.4 // indirect
 	github.com/fsnotify/fsnotify v1.8.0 // indirect
-	github.com/go-logr/logr v1.4.2 // indirect
-	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
-	github.com/gogo/protobuf v1.3.2 // indirect
+	github.com/google/go-cmp v0.7.0 // indirect
 	github.com/google/go-github/v64 v64.0.0 // indirect
 	github.com/google/go-querystring v1.1.0 // indirect
 	github.com/google/uuid v1.6.0 // indirect
 	github.com/gorilla/mux v1.8.0 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
-	github.com/moby/docker-image-spec v1.3.1 // indirect
-	github.com/moby/term v0.5.0 // indirect
-	github.com/morikuni/aec v1.0.0 // indirect
-	github.com/opencontainers/go-digest v1.0.0 // indirect
-	github.com/opencontainers/image-spec v1.1.1 // indirect
 	github.com/pelletier/go-toml/v2 v2.2.3 // indirect
-	github.com/pkg/errors v0.9.1 // indirect
 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
+	github.com/rogpeppe/go-internal v1.13.1 // indirect
 	github.com/sagikazarmark/locafero v0.9.0 // indirect
 	github.com/sourcegraph/conc v0.3.0 // indirect
 	github.com/spf13/afero v1.14.0 // indirect
@@ -47,20 +32,10 @@ require (
 	github.com/spf13/pflag v1.0.6 // indirect
 	github.com/subosito/gotenv v1.6.0 // indirect
 	github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
-	go.opentelemetry.io/auto/sdk v1.1.0 // indirect
-	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
-	go.opentelemetry.io/otel v1.35.0 // indirect
-	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect
-	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 // indirect
-	go.opentelemetry.io/otel/metric v1.35.0 // indirect
-	go.opentelemetry.io/otel/sdk v1.35.0 // indirect
-	go.opentelemetry.io/otel/trace v1.35.0 // indirect
-	go.opentelemetry.io/proto/otlp v1.5.0 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
 	golang.org/x/sys v0.31.0 // indirect
 	golang.org/x/text v0.23.0 // indirect
 	golang.org/x/time v0.5.0 // indirect
-	google.golang.org/protobuf v1.36.5 // indirect
+	gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
-	gotest.tools/v3 v3.5.1 // indirect
 )
diff --git a/go.sum b/go.sum
index 3378b4f..94ba995 100644
--- a/go.sum
+++ b/go.sum
@@ -1,39 +1,14 @@
-github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
-github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
-github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
-github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
-github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
-github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
-github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
-github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
 github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
-github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
-github.com/docker/docker v28.0.4+incompatible h1:JNNkBctYKurkw6FrHfKqY0nKIDf5nrbxjVBtS+cdcok=
-github.com/docker/docker v28.0.4+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
-github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
-github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
-github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
-github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
-github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
-github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
 github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
 github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
 github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
 github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
-github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
-github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
-github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
-github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
-github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
 github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
 github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
-github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
-github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
 github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
 github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
@@ -47,34 +22,21 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
 github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ=
 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
-github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
-github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
-github.com/mark3labs/mcp-go v0.20.1 h1:E1Bbx9K8d8kQmDZ1QHblM38c7UU2evQ2LlkANk1U/zw=
-github.com/mark3labs/mcp-go v0.20.1/go.mod h1:KmJndYv7GIgcPVwEKJjNcbhVQ+hJGJhrCCB/9xITzpE=
+github.com/mark3labs/mcp-go v0.22.0 h1:cCEBWi4Yy9Kio+OW1hWIyi4WLsSr+RBBK6FI5tj+b7I=
+github.com/mark3labs/mcp-go v0.22.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4=
 github.com/migueleliasweb/go-github-mock v1.1.0 h1:GKaOBPsrPGkAKgtfuWY8MclS1xR6MInkx1SexJucMwE=
 github.com/migueleliasweb/go-github-mock v1.1.0/go.mod h1:pYe/XlGs4BGMfRY4vmeixVsODHnVDDhJ9zoi0qzSMHc=
-github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
-github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
-github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
-github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
-github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
-github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
-github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
-github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
-github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
-github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
 github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
 github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
-github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
-github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -105,75 +67,19 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8
 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
 github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
 github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
-github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
-go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
-go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
-go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
-go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
-go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
-go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
-go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
-go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
-go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
-go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
-go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
 go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
 go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
-golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
-golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
-golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
 golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
 golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
 golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
 golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
-golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d h1:H8tOf8XM88HvKqLTxe755haY6r1fqqzLbEnfrmLXlSA=
-google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d/go.mod h1:2v7Z7gP2ZUOGsaFyxATQSRoBnKygqVq2Cwnvom7QiqY=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d h1:xJJRGY7TJcvIlpSrN3K6LAWgNFUILlO+OMAqtg9aqnw=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4=
-google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU=
-google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
-google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
-google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
-gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
diff --git a/pkg/github/code_scanning.go b/pkg/github/code_scanning.go
index 5dbac0d..93e7e0e 100644
--- a/pkg/github/code_scanning.go
+++ b/pkg/github/code_scanning.go
@@ -16,6 +16,10 @@ import (
 func GetCodeScanningAlert(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
 	return mcp.NewTool("get_code_scanning_alert",
 			mcp.WithDescription(t("TOOL_GET_CODE_SCANNING_ALERT_DESCRIPTION", "Get details of a specific code scanning alert in a GitHub repository.")),
+			mcp.WithToolAnnotation(mcp.ToolAnnotation{
+				Title:        t("TOOL_GET_CODE_SCANNING_ALERT_USER_TITLE", "Get code scanning alert"),
+				ReadOnlyHint: true,
+			}),
 			mcp.WithString("owner",
 				mcp.Required(),
 				mcp.Description("The owner of the repository."),
@@ -74,6 +78,10 @@ func GetCodeScanningAlert(getClient GetClientFn, t translations.TranslationHelpe
 func ListCodeScanningAlerts(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
 	return mcp.NewTool("list_code_scanning_alerts",
 			mcp.WithDescription(t("TOOL_LIST_CODE_SCANNING_ALERTS_DESCRIPTION", "List code scanning alerts in a GitHub repository.")),
+			mcp.WithToolAnnotation(mcp.ToolAnnotation{
+				Title:        t("TOOL_LIST_CODE_SCANNING_ALERTS_USER_TITLE", "List code scanning alerts"),
+				ReadOnlyHint: true,
+			}),
 			mcp.WithString("owner",
 				mcp.Required(),
 				mcp.Description("The owner of the repository."),
@@ -94,6 +102,9 @@ func ListCodeScanningAlerts(getClient GetClientFn, t translations.TranslationHel
 				mcp.Description("Filter code scanning alerts by severity"),
 				mcp.Enum("critical", "high", "medium", "low", "warning", "note", "error"),
 			),
+			mcp.WithString("tool_name",
+				mcp.Description("The name of the tool used for code scanning."),
+			),
 		),
 		func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 			owner, err := requiredParam[string](request, "owner")
@@ -116,12 +127,16 @@ func ListCodeScanningAlerts(getClient GetClientFn, t translations.TranslationHel
 			if err != nil {
 				return mcp.NewToolResultError(err.Error()), nil
 			}
+			toolName, err := OptionalParam[string](request, "tool_name")
+			if err != nil {
+				return mcp.NewToolResultError(err.Error()), nil
+			}
 
 			client, err := getClient(ctx)
 			if err != nil {
 				return nil, fmt.Errorf("failed to get GitHub client: %w", err)
 			}
-			alerts, resp, err := client.CodeScanning.ListAlertsForRepo(ctx, owner, repo, &github.AlertListOptions{Ref: ref, State: state, Severity: severity})
+			alerts, resp, err := client.CodeScanning.ListAlertsForRepo(ctx, owner, repo, &github.AlertListOptions{Ref: ref, State: state, Severity: severity, ToolName: toolName})
 			if err != nil {
 				return nil, fmt.Errorf("failed to list alerts: %w", err)
 			}
diff --git a/pkg/github/context_tools.go b/pkg/github/context_tools.go
index 1c91d70..3511e23 100644
--- a/pkg/github/context_tools.go
+++ b/pkg/github/context_tools.go
@@ -16,6 +16,10 @@ import (
 func GetMe(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
 	return mcp.NewTool("get_me",
 			mcp.WithDescription(t("TOOL_GET_ME_DESCRIPTION", "Get details of the authenticated GitHub user. Use this when a request include \"me\", \"my\"...")),
+			mcp.WithToolAnnotation(mcp.ToolAnnotation{
+				Title:        t("TOOL_GET_ME_USER_TITLE", "Get my user profile"),
+				ReadOnlyHint: true,
+			}),
 			mcp.WithString("reason",
 				mcp.Description("Optional: reason the session was created"),
 			),
diff --git a/pkg/github/dynamic_tools.go b/pkg/github/dynamic_tools.go
index d4d5f27..30dfd4a 100644
--- a/pkg/github/dynamic_tools.go
+++ b/pkg/github/dynamic_tools.go
@@ -22,6 +22,11 @@ func ToolsetEnum(toolsetGroup *toolsets.ToolsetGroup) mcp.PropertyOption {
 func EnableToolset(s *server.MCPServer, toolsetGroup *toolsets.ToolsetGroup, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
 	return mcp.NewTool("enable_toolset",
 			mcp.WithDescription(t("TOOL_ENABLE_TOOLSET_DESCRIPTION", "Enable one of the sets of tools the GitHub MCP server provides, use get_toolset_tools and list_available_toolsets first to see what this will enable")),
+			mcp.WithToolAnnotation(mcp.ToolAnnotation{
+				Title: t("TOOL_ENABLE_TOOLSET_USER_TITLE", "Enable a toolset"),
+				// Not modifying GitHub data so no need to show a warning
+				ReadOnlyHint: true,
+			}),
 			mcp.WithString("toolset",
 				mcp.Required(),
 				mcp.Description("The name of the toolset to enable"),
@@ -57,6 +62,10 @@ func EnableToolset(s *server.MCPServer, toolsetGroup *toolsets.ToolsetGroup, t t
 func ListAvailableToolsets(toolsetGroup *toolsets.ToolsetGroup, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
 	return mcp.NewTool("list_available_toolsets",
 			mcp.WithDescription(t("TOOL_LIST_AVAILABLE_TOOLSETS_DESCRIPTION", "List all available toolsets this GitHub MCP server can offer, providing the enabled status of each. Use this when a task could be achieved with a GitHub tool and the currently available tools aren't enough. Call get_toolset_tools with these toolset names to discover specific tools you can call")),
+			mcp.WithToolAnnotation(mcp.ToolAnnotation{
+				Title:        t("TOOL_LIST_AVAILABLE_TOOLSETS_USER_TITLE", "List available toolsets"),
+				ReadOnlyHint: true,
+			}),
 		),
 		func(_ context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 			// We need to convert the toolsetGroup back to a map for JSON serialization
@@ -87,6 +96,10 @@ func ListAvailableToolsets(toolsetGroup *toolsets.ToolsetGroup, t translations.T
 func GetToolsetsTools(toolsetGroup *toolsets.ToolsetGroup, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
 	return mcp.NewTool("get_toolset_tools",
 			mcp.WithDescription(t("TOOL_GET_TOOLSET_TOOLS_DESCRIPTION", "Lists all the capabilities that are enabled with the specified toolset, use this to get clarity on whether enabling a toolset would help you to complete a task")),
+			mcp.WithToolAnnotation(mcp.ToolAnnotation{
+				Title:        t("TOOL_GET_TOOLSET_TOOLS_USER_TITLE", "List all tools in a toolset"),
+				ReadOnlyHint: true,
+			}),
 			mcp.WithString("toolset",
 				mcp.Required(),
 				mcp.Description("The name of the toolset you want to get the tools for"),
diff --git a/pkg/github/issues.go b/pkg/github/issues.go
index 1324bd5..0fcc250 100644
--- a/pkg/github/issues.go
+++ b/pkg/github/issues.go
@@ -17,7 +17,11 @@ import (
 // GetIssue creates a tool to get details of a specific issue in a GitHub repository.
 func GetIssue(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
 	return mcp.NewTool("get_issue",
-			mcp.WithDescription(t("TOOL_GET_ISSUE_DESCRIPTION", "Get details of a specific issue in a GitHub repository")),
+			mcp.WithDescription(t("TOOL_GET_ISSUE_DESCRIPTION", "Get details of a specific issue in a GitHub repository.")),
+			mcp.WithToolAnnotation(mcp.ToolAnnotation{
+				Title:        t("TOOL_GET_ISSUE_USER_TITLE", "Get issue details"),
+				ReadOnlyHint: true,
+			}),
 			mcp.WithString("owner",
 				mcp.Required(),
 				mcp.Description("The owner of the repository"),
@@ -75,7 +79,11 @@ func GetIssue(getClient GetClientFn, t translations.TranslationHelperFunc) (tool
 // AddIssueComment creates a tool to add a comment to an issue.
 func AddIssueComment(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
 	return mcp.NewTool("add_issue_comment",
-			mcp.WithDescription(t("TOOL_ADD_ISSUE_COMMENT_DESCRIPTION", "Add a comment to an existing issue")),
+			mcp.WithDescription(t("TOOL_ADD_ISSUE_COMMENT_DESCRIPTION", "Add a comment to a specific issue in a GitHub repository.")),
+			mcp.WithToolAnnotation(mcp.ToolAnnotation{
+				Title:        t("TOOL_ADD_ISSUE_COMMENT_USER_TITLE", "Add comment to issue"),
+				ReadOnlyHint: false,
+			}),
 			mcp.WithString("owner",
 				mcp.Required(),
 				mcp.Description("Repository owner"),
@@ -145,7 +153,11 @@ func AddIssueComment(getClient GetClientFn, t translations.TranslationHelperFunc
 // SearchIssues creates a tool to search for issues and pull requests.
 func SearchIssues(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
 	return mcp.NewTool("search_issues",
-			mcp.WithDescription(t("TOOL_SEARCH_ISSUES_DESCRIPTION", "Search for issues and pull requests across GitHub repositories")),
+			mcp.WithDescription(t("TOOL_SEARCH_ISSUES_DESCRIPTION", "Search for issues in GitHub repositories.")),
+			mcp.WithToolAnnotation(mcp.ToolAnnotation{
+				Title:        t("TOOL_SEARCH_ISSUES_USER_TITLE", "Search issues"),
+				ReadOnlyHint: true,
+			}),
 			mcp.WithString("q",
 				mcp.Required(),
 				mcp.Description("Search query using GitHub issues search syntax"),
@@ -229,7 +241,11 @@ func SearchIssues(getClient GetClientFn, t translations.TranslationHelperFunc) (
 // CreateIssue creates a tool to create a new issue in a GitHub repository.
 func CreateIssue(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
 	return mcp.NewTool("create_issue",
-			mcp.WithDescription(t("TOOL_CREATE_ISSUE_DESCRIPTION", "Create a new issue in a GitHub repository")),
+			mcp.WithDescription(t("TOOL_CREATE_ISSUE_DESCRIPTION", "Create a new issue in a GitHub repository.")),
+			mcp.WithToolAnnotation(mcp.ToolAnnotation{
+				Title:        t("TOOL_CREATE_ISSUE_USER_TITLE", "Open new issue"),
+				ReadOnlyHint: false,
+			}),
 			mcp.WithString("owner",
 				mcp.Required(),
 				mcp.Description("Repository owner"),
@@ -347,7 +363,11 @@ func CreateIssue(getClient GetClientFn, t translations.TranslationHelperFunc) (t
 // ListIssues creates a tool to list and filter repository issues
 func ListIssues(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
 	return mcp.NewTool("list_issues",
-			mcp.WithDescription(t("TOOL_LIST_ISSUES_DESCRIPTION", "List issues in a GitHub repository with filtering options")),
+			mcp.WithDescription(t("TOOL_LIST_ISSUES_DESCRIPTION", "List issues in a GitHub repository.")),
+			mcp.WithToolAnnotation(mcp.ToolAnnotation{
+				Title:        t("TOOL_LIST_ISSUES_USER_TITLE", "List issues"),
+				ReadOnlyHint: true,
+			}),
 			mcp.WithString("owner",
 				mcp.Required(),
 				mcp.Description("Repository owner"),
@@ -465,7 +485,11 @@ func ListIssues(getClient GetClientFn, t translations.TranslationHelperFunc) (to
 // UpdateIssue creates a tool to update an existing issue in a GitHub repository.
 func UpdateIssue(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
 	return mcp.NewTool("update_issue",
-			mcp.WithDescription(t("TOOL_UPDATE_ISSUE_DESCRIPTION", "Update an existing issue in a GitHub repository")),
+			mcp.WithDescription(t("TOOL_UPDATE_ISSUE_DESCRIPTION", "Update an existing issue in a GitHub repository.")),
+			mcp.WithToolAnnotation(mcp.ToolAnnotation{
+				Title:        t("TOOL_UPDATE_ISSUE_USER_TITLE", "Edit issue"),
+				ReadOnlyHint: false,
+			}),
 			mcp.WithString("owner",
 				mcp.Required(),
 				mcp.Description("Repository owner"),
@@ -607,7 +631,11 @@ func UpdateIssue(getClient GetClientFn, t translations.TranslationHelperFunc) (t
 // GetIssueComments creates a tool to get comments for a GitHub issue.
 func GetIssueComments(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
 	return mcp.NewTool("get_issue_comments",
-			mcp.WithDescription(t("TOOL_GET_ISSUE_COMMENTS_DESCRIPTION", "Get comments for a GitHub issue")),
+			mcp.WithDescription(t("TOOL_GET_ISSUE_COMMENTS_DESCRIPTION", "Get comments for a specific issue in a GitHub repository.")),
+			mcp.WithToolAnnotation(mcp.ToolAnnotation{
+				Title:        t("TOOL_GET_ISSUE_COMMENTS_USER_TITLE", "Get issue comments"),
+				ReadOnlyHint: true,
+			}),
 			mcp.WithString("owner",
 				mcp.Required(),
 				mcp.Description("Repository owner"),
diff --git a/pkg/github/pullrequests.go b/pkg/github/pullrequests.go
index 2be249c..9c8fca1 100644
--- a/pkg/github/pullrequests.go
+++ b/pkg/github/pullrequests.go
@@ -16,7 +16,11 @@ import (
 // GetPullRequest creates a tool to get details of a specific pull request.
 func GetPullRequest(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
 	return mcp.NewTool("get_pull_request",
-			mcp.WithDescription(t("TOOL_GET_PULL_REQUEST_DESCRIPTION", "Get details of a specific pull request")),
+			mcp.WithDescription(t("TOOL_GET_PULL_REQUEST_DESCRIPTION", "Get details of a specific pull request in a GitHub repository.")),
+			mcp.WithToolAnnotation(mcp.ToolAnnotation{
+				Title:        t("TOOL_GET_PULL_REQUEST_USER_TITLE", "Get pull request details"),
+				ReadOnlyHint: true,
+			}),
 			mcp.WithString("owner",
 				mcp.Required(),
 				mcp.Description("Repository owner"),
@@ -74,7 +78,11 @@ func GetPullRequest(getClient GetClientFn, t translations.TranslationHelperFunc)
 // UpdatePullRequest creates a tool to update an existing pull request.
 func UpdatePullRequest(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
 	return mcp.NewTool("update_pull_request",
-			mcp.WithDescription(t("TOOL_UPDATE_PULL_REQUEST_DESCRIPTION", "Update an existing pull request in a GitHub repository")),
+			mcp.WithDescription(t("TOOL_UPDATE_PULL_REQUEST_DESCRIPTION", "Update an existing pull request in a GitHub repository.")),
+			mcp.WithToolAnnotation(mcp.ToolAnnotation{
+				Title:        t("TOOL_UPDATE_PULL_REQUEST_USER_TITLE", "Edit pull request"),
+				ReadOnlyHint: false,
+			}),
 			mcp.WithString("owner",
 				mcp.Required(),
 				mcp.Description("Repository owner"),
@@ -191,7 +199,11 @@ func UpdatePullRequest(getClient GetClientFn, t translations.TranslationHelperFu
 // ListPullRequests creates a tool to list and filter repository pull requests.
 func ListPullRequests(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
 	return mcp.NewTool("list_pull_requests",
-			mcp.WithDescription(t("TOOL_LIST_PULL_REQUESTS_DESCRIPTION", "List and filter repository pull requests")),
+			mcp.WithDescription(t("TOOL_LIST_PULL_REQUESTS_DESCRIPTION", "List pull requests in a GitHub repository.")),
+			mcp.WithToolAnnotation(mcp.ToolAnnotation{
+				Title:        t("TOOL_LIST_PULL_REQUESTS_USER_TITLE", "List pull requests"),
+				ReadOnlyHint: true,
+			}),
 			mcp.WithString("owner",
 				mcp.Required(),
 				mcp.Description("Repository owner"),
@@ -296,7 +308,11 @@ func ListPullRequests(getClient GetClientFn, t translations.TranslationHelperFun
 // MergePullRequest creates a tool to merge a pull request.
 func MergePullRequest(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
 	return mcp.NewTool("merge_pull_request",
-			mcp.WithDescription(t("TOOL_MERGE_PULL_REQUEST_DESCRIPTION", "Merge a pull request")),
+			mcp.WithDescription(t("TOOL_MERGE_PULL_REQUEST_DESCRIPTION", "Merge a pull request in a GitHub repository.")),
+			mcp.WithToolAnnotation(mcp.ToolAnnotation{
+				Title:        t("TOOL_MERGE_PULL_REQUEST_USER_TITLE", "Merge pull request"),
+				ReadOnlyHint: false,
+			}),
 			mcp.WithString("owner",
 				mcp.Required(),
 				mcp.Description("Repository owner"),
@@ -381,7 +397,11 @@ func MergePullRequest(getClient GetClientFn, t translations.TranslationHelperFun
 // GetPullRequestFiles creates a tool to get the list of files changed in a pull request.
 func GetPullRequestFiles(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
 	return mcp.NewTool("get_pull_request_files",
-			mcp.WithDescription(t("TOOL_GET_PULL_REQUEST_FILES_DESCRIPTION", "Get the list of files changed in a pull request")),
+			mcp.WithDescription(t("TOOL_GET_PULL_REQUEST_FILES_DESCRIPTION", "Get the files changed in a specific pull request.")),
+			mcp.WithToolAnnotation(mcp.ToolAnnotation{
+				Title:        t("TOOL_GET_PULL_REQUEST_FILES_USER_TITLE", "Get pull request files"),
+				ReadOnlyHint: true,
+			}),
 			mcp.WithString("owner",
 				mcp.Required(),
 				mcp.Description("Repository owner"),
@@ -440,7 +460,11 @@ func GetPullRequestFiles(getClient GetClientFn, t translations.TranslationHelper
 // GetPullRequestStatus creates a tool to get the combined status of all status checks for a pull request.
 func GetPullRequestStatus(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
 	return mcp.NewTool("get_pull_request_status",
-			mcp.WithDescription(t("TOOL_GET_PULL_REQUEST_STATUS_DESCRIPTION", "Get the combined status of all status checks for a pull request")),
+			mcp.WithDescription(t("TOOL_GET_PULL_REQUEST_STATUS_DESCRIPTION", "Get the status of a specific pull request.")),
+			mcp.WithToolAnnotation(mcp.ToolAnnotation{
+				Title:        t("TOOL_GET_PULL_REQUEST_STATUS_USER_TITLE", "Get pull request status checks"),
+				ReadOnlyHint: true,
+			}),
 			mcp.WithString("owner",
 				mcp.Required(),
 				mcp.Description("Repository owner"),
@@ -513,7 +537,11 @@ func GetPullRequestStatus(getClient GetClientFn, t translations.TranslationHelpe
 // UpdatePullRequestBranch creates a tool to update a pull request branch with the latest changes from the base branch.
 func UpdatePullRequestBranch(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
 	return mcp.NewTool("update_pull_request_branch",
-			mcp.WithDescription(t("TOOL_UPDATE_PULL_REQUEST_BRANCH_DESCRIPTION", "Update a pull request branch with the latest changes from the base branch")),
+			mcp.WithDescription(t("TOOL_UPDATE_PULL_REQUEST_BRANCH_DESCRIPTION", "Update the branch of a pull request with the latest changes from the base branch.")),
+			mcp.WithToolAnnotation(mcp.ToolAnnotation{
+				Title:        t("TOOL_UPDATE_PULL_REQUEST_BRANCH_USER_TITLE", "Update pull request branch"),
+				ReadOnlyHint: false,
+			}),
 			mcp.WithString("owner",
 				mcp.Required(),
 				mcp.Description("Repository owner"),
@@ -587,7 +615,11 @@ func UpdatePullRequestBranch(getClient GetClientFn, t translations.TranslationHe
 // GetPullRequestComments creates a tool to get the review comments on a pull request.
 func GetPullRequestComments(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
 	return mcp.NewTool("get_pull_request_comments",
-			mcp.WithDescription(t("TOOL_GET_PULL_REQUEST_COMMENTS_DESCRIPTION", "Get the review comments on a pull request")),
+			mcp.WithDescription(t("TOOL_GET_PULL_REQUEST_COMMENTS_DESCRIPTION", "Get comments for a specific pull request.")),
+			mcp.WithToolAnnotation(mcp.ToolAnnotation{
+				Title:        t("TOOL_GET_PULL_REQUEST_COMMENTS_USER_TITLE", "Get pull request comments"),
+				ReadOnlyHint: true,
+			}),
 			mcp.WithString("owner",
 				mcp.Required(),
 				mcp.Description("Repository owner"),
@@ -651,7 +683,11 @@ func GetPullRequestComments(getClient GetClientFn, t translations.TranslationHel
 // AddPullRequestReviewComment creates a tool to add a review comment to a pull request.
 func AddPullRequestReviewComment(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
 	return mcp.NewTool("add_pull_request_review_comment",
-			mcp.WithDescription(t("TOOL_ADD_PULL_REQUEST_COMMENT_DESCRIPTION", "Add a review comment to a pull request")),
+			mcp.WithDescription(t("TOOL_ADD_PULL_REQUEST_REVIEW_COMMENT_DESCRIPTION", "Add a review comment to a pull request.")),
+			mcp.WithToolAnnotation(mcp.ToolAnnotation{
+				Title:        t("TOOL_ADD_PULL_REQUEST_REVIEW_COMMENT_USER_TITLE", "Add review comment to pull request"),
+				ReadOnlyHint: false,
+			}),
 			mcp.WithString("owner",
 				mcp.Required(),
 				mcp.Description("Repository owner"),
@@ -821,7 +857,11 @@ func AddPullRequestReviewComment(getClient GetClientFn, t translations.Translati
 // GetPullRequestReviews creates a tool to get the reviews on a pull request.
 func GetPullRequestReviews(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
 	return mcp.NewTool("get_pull_request_reviews",
-			mcp.WithDescription(t("TOOL_GET_PULL_REQUEST_REVIEWS_DESCRIPTION", "Get the reviews on a pull request")),
+			mcp.WithDescription(t("TOOL_GET_PULL_REQUEST_REVIEWS_DESCRIPTION", "Get reviews for a specific pull request.")),
+			mcp.WithToolAnnotation(mcp.ToolAnnotation{
+				Title:        t("TOOL_GET_PULL_REQUEST_REVIEWS_USER_TITLE", "Get pull request reviews"),
+				ReadOnlyHint: true,
+			}),
 			mcp.WithString("owner",
 				mcp.Required(),
 				mcp.Description("Repository owner"),
@@ -879,7 +919,11 @@ func GetPullRequestReviews(getClient GetClientFn, t translations.TranslationHelp
 // CreatePullRequestReview creates a tool to submit a review on a pull request.
 func CreatePullRequestReview(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
 	return mcp.NewTool("create_pull_request_review",
-			mcp.WithDescription(t("TOOL_CREATE_PULL_REQUEST_REVIEW_DESCRIPTION", "Create a review on a pull request")),
+			mcp.WithDescription(t("TOOL_CREATE_PULL_REQUEST_REVIEW_DESCRIPTION", "Create a review for a pull request.")),
+			mcp.WithToolAnnotation(mcp.ToolAnnotation{
+				Title:        t("TOOL_CREATE_PULL_REQUEST_REVIEW_USER_TITLE", "Submit pull request review"),
+				ReadOnlyHint: false,
+			}),
 			mcp.WithString("owner",
 				mcp.Required(),
 				mcp.Description("Repository owner"),
@@ -908,30 +952,45 @@ func CreatePullRequestReview(getClient GetClientFn, t translations.TranslationHe
 					map[string]interface{}{
 						"type":                 "object",
 						"additionalProperties": false,
-						"required":             []string{"path", "body"},
+						"required":             []string{"path", "body", "position", "line", "side", "start_line", "start_side"},
 						"properties": map[string]interface{}{
 							"path": map[string]interface{}{
 								"type":        "string",
 								"description": "path to the file",
 							},
 							"position": map[string]interface{}{
-								"type":        "number",
+								"anyOf": []interface{}{
+									map[string]string{"type": "number"},
+									map[string]string{"type": "null"},
+								},
 								"description": "position of the comment in the diff",
 							},
 							"line": map[string]interface{}{
-								"type":        "number",
+								"anyOf": []interface{}{
+									map[string]string{"type": "number"},
+									map[string]string{"type": "null"},
+								},
 								"description": "line number in the file to comment on. For multi-line comments, the end of the line range",
 							},
 							"side": map[string]interface{}{
-								"type":        "string",
+								"anyOf": []interface{}{
+									map[string]string{"type": "string"},
+									map[string]string{"type": "null"},
+								},
 								"description": "The side of the diff on which the line resides. For multi-line comments, this is the side for the end of the line range. (LEFT or RIGHT)",
 							},
 							"start_line": map[string]interface{}{
-								"type":        "number",
+								"anyOf": []interface{}{
+									map[string]string{"type": "number"},
+									map[string]string{"type": "null"},
+								},
 								"description": "The first line of the range to which the comment refers. Required for multi-line comments.",
 							},
 							"start_side": map[string]interface{}{
-								"type":        "string",
+								"anyOf": []interface{}{
+									map[string]string{"type": "string"},
+									map[string]string{"type": "null"},
+								},
 								"description": "The side of the diff on which the start line resides for multi-line comments. (LEFT or RIGHT)",
 							},
 							"body": map[string]interface{}{
@@ -1076,7 +1135,11 @@ func CreatePullRequestReview(getClient GetClientFn, t translations.TranslationHe
 // CreatePullRequest creates a tool to create a new pull request.
 func CreatePullRequest(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
 	return mcp.NewTool("create_pull_request",
-			mcp.WithDescription(t("TOOL_CREATE_PULL_REQUEST_DESCRIPTION", "Create a new pull request in a GitHub repository")),
+			mcp.WithDescription(t("TOOL_CREATE_PULL_REQUEST_DESCRIPTION", "Create a new pull request in a GitHub repository.")),
+			mcp.WithToolAnnotation(mcp.ToolAnnotation{
+				Title:        t("TOOL_CREATE_PULL_REQUEST_USER_TITLE", "Open new pull request"),
+				ReadOnlyHint: false,
+			}),
 			mcp.WithString("owner",
 				mcp.Required(),
 				mcp.Description("Repository owner"),
diff --git a/pkg/github/repositories.go b/pkg/github/repositories.go
index 5194873..2ef328a 100644
--- a/pkg/github/repositories.go
+++ b/pkg/github/repositories.go
@@ -16,6 +16,10 @@ import (
 func GetCommit(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
 	return mcp.NewTool("get_commit",
 			mcp.WithDescription(t("TOOL_GET_COMMITS_DESCRIPTION", "Get details for a commit from a GitHub repository")),
+			mcp.WithToolAnnotation(mcp.ToolAnnotation{
+				Title:        t("TOOL_GET_COMMITS_USER_TITLE", "Get commit details"),
+				ReadOnlyHint: true,
+			}),
 			mcp.WithString("owner",
 				mcp.Required(),
 				mcp.Description("Repository owner"),
@@ -84,6 +88,10 @@ func GetCommit(getClient GetClientFn, t translations.TranslationHelperFunc) (too
 func ListCommits(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
 	return mcp.NewTool("list_commits",
 			mcp.WithDescription(t("TOOL_LIST_COMMITS_DESCRIPTION", "Get list of commits of a branch in a GitHub repository")),
+			mcp.WithToolAnnotation(mcp.ToolAnnotation{
+				Title:        t("TOOL_LIST_COMMITS_USER_TITLE", "List commits"),
+				ReadOnlyHint: true,
+			}),
 			mcp.WithString("owner",
 				mcp.Required(),
 				mcp.Description("Repository owner"),
@@ -154,6 +162,10 @@ func ListCommits(getClient GetClientFn, t translations.TranslationHelperFunc) (t
 func ListBranches(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
 	return mcp.NewTool("list_branches",
 			mcp.WithDescription(t("TOOL_LIST_BRANCHES_DESCRIPTION", "List branches in a GitHub repository")),
+			mcp.WithToolAnnotation(mcp.ToolAnnotation{
+				Title:        t("TOOL_LIST_BRANCHES_USER_TITLE", "List branches"),
+				ReadOnlyHint: true,
+			}),
 			mcp.WithString("owner",
 				mcp.Required(),
 				mcp.Description("Repository owner"),
@@ -217,6 +229,10 @@ func ListBranches(getClient GetClientFn, t translations.TranslationHelperFunc) (
 func CreateOrUpdateFile(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
 	return mcp.NewTool("create_or_update_file",
 			mcp.WithDescription(t("TOOL_CREATE_OR_UPDATE_FILE_DESCRIPTION", "Create or update a single file in a GitHub repository")),
+			mcp.WithToolAnnotation(mcp.ToolAnnotation{
+				Title:        t("TOOL_CREATE_OR_UPDATE_FILE_USER_TITLE", "Create or update file"),
+				ReadOnlyHint: false,
+			}),
 			mcp.WithString("owner",
 				mcp.Required(),
 				mcp.Description("Repository owner (username or organization)"),
@@ -322,6 +338,10 @@ func CreateOrUpdateFile(getClient GetClientFn, t translations.TranslationHelperF
 func CreateRepository(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
 	return mcp.NewTool("create_repository",
 			mcp.WithDescription(t("TOOL_CREATE_REPOSITORY_DESCRIPTION", "Create a new GitHub repository in your account")),
+			mcp.WithToolAnnotation(mcp.ToolAnnotation{
+				Title:        t("TOOL_CREATE_REPOSITORY_USER_TITLE", "Create repository"),
+				ReadOnlyHint: false,
+			}),
 			mcp.WithString("name",
 				mcp.Required(),
 				mcp.Description("Repository name"),
@@ -392,6 +412,10 @@ func CreateRepository(getClient GetClientFn, t translations.TranslationHelperFun
 func GetFileContents(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
 	return mcp.NewTool("get_file_contents",
 			mcp.WithDescription(t("TOOL_GET_FILE_CONTENTS_DESCRIPTION", "Get the contents of a file or directory from a GitHub repository")),
+			mcp.WithToolAnnotation(mcp.ToolAnnotation{
+				Title:        t("TOOL_GET_FILE_CONTENTS_USER_TITLE", "Get file or directory contents"),
+				ReadOnlyHint: true,
+			}),
 			mcp.WithString("owner",
 				mcp.Required(),
 				mcp.Description("Repository owner (username or organization)"),
@@ -465,6 +489,10 @@ func GetFileContents(getClient GetClientFn, t translations.TranslationHelperFunc
 func ForkRepository(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
 	return mcp.NewTool("fork_repository",
 			mcp.WithDescription(t("TOOL_FORK_REPOSITORY_DESCRIPTION", "Fork a GitHub repository to your account or specified organization")),
+			mcp.WithToolAnnotation(mcp.ToolAnnotation{
+				Title:        t("TOOL_FORK_REPOSITORY_USER_TITLE", "Fork repository"),
+				ReadOnlyHint: false,
+			}),
 			mcp.WithString("owner",
 				mcp.Required(),
 				mcp.Description("Repository owner"),
@@ -532,6 +560,10 @@ func ForkRepository(getClient GetClientFn, t translations.TranslationHelperFunc)
 func CreateBranch(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
 	return mcp.NewTool("create_branch",
 			mcp.WithDescription(t("TOOL_CREATE_BRANCH_DESCRIPTION", "Create a new branch in a GitHub repository")),
+			mcp.WithToolAnnotation(mcp.ToolAnnotation{
+				Title:        t("TOOL_CREATE_BRANCH_USER_TITLE", "Create branch"),
+				ReadOnlyHint: false,
+			}),
 			mcp.WithString("owner",
 				mcp.Required(),
 				mcp.Description("Repository owner"),
@@ -580,7 +612,7 @@ func CreateBranch(getClient GetClientFn, t translations.TranslationHelperFunc) (
 				if err != nil {
 					return nil, fmt.Errorf("failed to get repository: %w", err)
 				}
-				defer func() { _ = resp.Body.Close() }()
+				defer resp.Body.Close()
 
 				fromBranch = *repository.DefaultBranch
 			}
@@ -590,7 +622,7 @@ func CreateBranch(getClient GetClientFn, t translations.TranslationHelperFunc) (
 			if err != nil {
 				return nil, fmt.Errorf("failed to get reference: %w", err)
 			}
-			defer func() { _ = resp.Body.Close() }()
+			defer resp.Body.Close()
 
 			// Create new branch
 			newRef := &github.Reference{
@@ -602,7 +634,7 @@ func CreateBranch(getClient GetClientFn, t translations.TranslationHelperFunc) (
 			if err != nil {
 				return nil, fmt.Errorf("failed to create branch: %w", err)
 			}
-			defer func() { _ = resp.Body.Close() }()
+			defer resp.Body.Close()
 
 			r, err := json.Marshal(createdRef)
 			if err != nil {
@@ -617,6 +649,10 @@ func CreateBranch(getClient GetClientFn, t translations.TranslationHelperFunc) (
 func PushFiles(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
 	return mcp.NewTool("push_files",
 			mcp.WithDescription(t("TOOL_PUSH_FILES_DESCRIPTION", "Push multiple files to a GitHub repository in a single commit")),
+			mcp.WithToolAnnotation(mcp.ToolAnnotation{
+				Title:        t("TOOL_PUSH_FILES_USER_TITLE", "Push files to repository"),
+				ReadOnlyHint: false,
+			}),
 			mcp.WithString("owner",
 				mcp.Required(),
 				mcp.Description("Repository owner"),
diff --git a/pkg/github/search.go b/pkg/github/search.go
index dc85c17..86a4f43 100644
--- a/pkg/github/search.go
+++ b/pkg/github/search.go
@@ -16,6 +16,10 @@ import (
 func SearchRepositories(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
 	return mcp.NewTool("search_repositories",
 			mcp.WithDescription(t("TOOL_SEARCH_REPOSITORIES_DESCRIPTION", "Search for GitHub repositories")),
+			mcp.WithToolAnnotation(mcp.ToolAnnotation{
+				Title:        t("TOOL_SEARCH_REPOSITORIES_USER_TITLE", "Search repositories"),
+				ReadOnlyHint: true,
+			}),
 			mcp.WithString("query",
 				mcp.Required(),
 				mcp.Description("Search query"),
@@ -70,6 +74,10 @@ func SearchRepositories(getClient GetClientFn, t translations.TranslationHelperF
 func SearchCode(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
 	return mcp.NewTool("search_code",
 			mcp.WithDescription(t("TOOL_SEARCH_CODE_DESCRIPTION", "Search for code across GitHub repositories")),
+			mcp.WithToolAnnotation(mcp.ToolAnnotation{
+				Title:        t("TOOL_SEARCH_CODE_USER_TITLE", "Search code"),
+				ReadOnlyHint: true,
+			}),
 			mcp.WithString("q",
 				mcp.Required(),
 				mcp.Description("Search query using GitHub code search syntax"),
@@ -142,6 +150,10 @@ func SearchCode(getClient GetClientFn, t translations.TranslationHelperFunc) (to
 func SearchUsers(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
 	return mcp.NewTool("search_users",
 			mcp.WithDescription(t("TOOL_SEARCH_USERS_DESCRIPTION", "Search for GitHub users")),
+			mcp.WithToolAnnotation(mcp.ToolAnnotation{
+				Title:        t("TOOL_SEARCH_USERS_USER_TITLE", "Search users"),
+				ReadOnlyHint: true,
+			}),
 			mcp.WithString("q",
 				mcp.Required(),
 				mcp.Description("Search query using GitHub users search syntax"),
diff --git a/pkg/github/secret_scanning.go b/pkg/github/secret_scanning.go
new file mode 100644
index 0000000..cd0fd04
--- /dev/null
+++ b/pkg/github/secret_scanning.go
@@ -0,0 +1,154 @@
+package github
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+
+	"github.com/github/github-mcp-server/pkg/translations"
+	"github.com/google/go-github/v69/github"
+	"github.com/mark3labs/mcp-go/mcp"
+	"github.com/mark3labs/mcp-go/server"
+)
+
+func GetSecretScanningAlert(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
+	return mcp.NewTool(
+			"get_secret_scanning_alert",
+			mcp.WithDescription(t("TOOL_GET_SECRET_SCANNING_ALERT_DESCRIPTION", "Get details of a specific secret scanning alert in a GitHub repository.")),
+			mcp.WithToolAnnotation(mcp.ToolAnnotation{
+				Title:        t("TOOL_GET_SECRET_SCANNING_ALERT_USER_TITLE", "Get secret scanning alert"),
+				ReadOnlyHint: true,
+			}),
+			mcp.WithString("owner",
+				mcp.Required(),
+				mcp.Description("The owner of the repository."),
+			),
+			mcp.WithString("repo",
+				mcp.Required(),
+				mcp.Description("The name of the repository."),
+			),
+			mcp.WithNumber("alertNumber",
+				mcp.Required(),
+				mcp.Description("The number of the alert."),
+			),
+		),
+		func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+			owner, err := requiredParam[string](request, "owner")
+			if err != nil {
+				return mcp.NewToolResultError(err.Error()), nil
+			}
+			repo, err := requiredParam[string](request, "repo")
+			if err != nil {
+				return mcp.NewToolResultError(err.Error()), nil
+			}
+			alertNumber, err := RequiredInt(request, "alertNumber")
+			if err != nil {
+				return mcp.NewToolResultError(err.Error()), nil
+			}
+
+			client, err := getClient(ctx)
+			if err != nil {
+				return nil, fmt.Errorf("failed to get GitHub client: %w", err)
+			}
+
+			alert, resp, err := client.SecretScanning.GetAlert(ctx, owner, repo, int64(alertNumber))
+			if err != nil {
+				return nil, fmt.Errorf("failed to get alert: %w", err)
+			}
+			defer func() { _ = resp.Body.Close() }()
+
+			if resp.StatusCode != http.StatusOK {
+				body, err := io.ReadAll(resp.Body)
+				if err != nil {
+					return nil, fmt.Errorf("failed to read response body: %w", err)
+				}
+				return mcp.NewToolResultError(fmt.Sprintf("failed to get alert: %s", string(body))), nil
+			}
+
+			r, err := json.Marshal(alert)
+			if err != nil {
+				return nil, fmt.Errorf("failed to marshal alert: %w", err)
+			}
+
+			return mcp.NewToolResultText(string(r)), nil
+		}
+}
+
+func ListSecretScanningAlerts(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
+	return mcp.NewTool(
+			"list_secret_scanning_alerts",
+			mcp.WithDescription(t("TOOL_LIST_SECRET_SCANNING_ALERTS_DESCRIPTION", "List secret scanning alerts in a GitHub repository.")),
+			mcp.WithToolAnnotation(mcp.ToolAnnotation{
+				Title:        t("TOOL_LIST_SECRET_SCANNING_ALERTS_USER_TITLE", "List secret scanning alerts"),
+				ReadOnlyHint: true,
+			}),
+			mcp.WithString("owner",
+				mcp.Required(),
+				mcp.Description("The owner of the repository."),
+			),
+			mcp.WithString("repo",
+				mcp.Required(),
+				mcp.Description("The name of the repository."),
+			),
+			mcp.WithString("state",
+				mcp.Description("Filter by state"),
+				mcp.Enum("open", "resolved"),
+			),
+			mcp.WithString("secret_type",
+				mcp.Description("A comma-separated list of secret types to return. All default secret patterns are returned. To return generic patterns, pass the token name(s) in the parameter."),
+			),
+			mcp.WithString("resolution",
+				mcp.Description("Filter by resolution"),
+				mcp.Enum("false_positive", "wont_fix", "revoked", "pattern_edited", "pattern_deleted", "used_in_tests"),
+			),
+		),
+		func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+			owner, err := requiredParam[string](request, "owner")
+			if err != nil {
+				return mcp.NewToolResultError(err.Error()), nil
+			}
+			repo, err := requiredParam[string](request, "repo")
+			if err != nil {
+				return mcp.NewToolResultError(err.Error()), nil
+			}
+			state, err := OptionalParam[string](request, "state")
+			if err != nil {
+				return mcp.NewToolResultError(err.Error()), nil
+			}
+			secretType, err := OptionalParam[string](request, "secret_type")
+			if err != nil {
+				return mcp.NewToolResultError(err.Error()), nil
+			}
+			resolution, err := OptionalParam[string](request, "resolution")
+			if err != nil {
+				return mcp.NewToolResultError(err.Error()), nil
+			}
+
+			client, err := getClient(ctx)
+			if err != nil {
+				return nil, fmt.Errorf("failed to get GitHub client: %w", err)
+			}
+			alerts, resp, err := client.SecretScanning.ListAlertsForRepo(ctx, owner, repo, &github.SecretScanningAlertListOptions{State: state, SecretType: secretType, Resolution: resolution})
+			if err != nil {
+				return nil, fmt.Errorf("failed to list alerts: %w", err)
+			}
+			defer func() { _ = resp.Body.Close() }()
+
+			if resp.StatusCode != http.StatusOK {
+				body, err := io.ReadAll(resp.Body)
+				if err != nil {
+					return nil, fmt.Errorf("failed to read response body: %w", err)
+				}
+				return mcp.NewToolResultError(fmt.Sprintf("failed to list alerts: %s", string(body))), nil
+			}
+
+			r, err := json.Marshal(alerts)
+			if err != nil {
+				return nil, fmt.Errorf("failed to marshal alerts: %w", err)
+			}
+
+			return mcp.NewToolResultText(string(r)), nil
+		}
+}
diff --git a/pkg/github/tools.go b/pkg/github/tools.go
index ce10c4a..1a4a3b4 100644
--- a/pkg/github/tools.go
+++ b/pkg/github/tools.go
@@ -73,6 +73,11 @@ func InitToolsets(passedToolsets []string, readOnly bool, getClient GetClientFn,
 			toolsets.NewServerTool(GetCodeScanningAlert(getClient, t)),
 			toolsets.NewServerTool(ListCodeScanningAlerts(getClient, t)),
 		)
+	secretProtection := toolsets.NewToolset("secret_protection", "Secret protection related tools, such as GitHub Secret Scanning").
+		AddReadTools(
+			toolsets.NewServerTool(GetSecretScanningAlert(getClient, t)),
+			toolsets.NewServerTool(ListSecretScanningAlerts(getClient, t)),
+		)
 	// Keep experiments alive so the system doesn't error out when it's always enabled
 	experiments := toolsets.NewToolset("experiments", "Experimental features that are not considered stable yet")
 
@@ -82,6 +87,7 @@ func InitToolsets(passedToolsets []string, readOnly bool, getClient GetClientFn,
 	tsg.AddToolset(users)
 	tsg.AddToolset(pullRequests)
 	tsg.AddToolset(codeSecurity)
+	tsg.AddToolset(secretProtection)
 	tsg.AddToolset(experiments)
 	// Enable the requested features
 
@@ -112,6 +118,7 @@ func InitDynamicToolset(s *server.MCPServer, tsg *toolsets.ToolsetGroup, t trans
 			toolsets.NewServerTool(GetToolsetsTools(tsg, t)),
 			toolsets.NewServerTool(EnableToolset(s, tsg, t)),
 		)
+
 	dynamicToolSelection.Enabled = true
 	return dynamicToolSelection
 }
diff --git a/pkg/toolsets/toolsets.go b/pkg/toolsets/toolsets.go
index d4397fc..b316aae 100644
--- a/pkg/toolsets/toolsets.go
+++ b/pkg/toolsets/toolsets.go
@@ -58,6 +58,11 @@ func (t *Toolset) SetReadOnly() {
 
 func (t *Toolset) AddWriteTools(tools ...server.ServerTool) *Toolset {
 	// Silently ignore if the toolset is read-only to avoid any breach of that contract
+	for _, tool := range tools {
+		if tool.Tool.Annotations.ReadOnlyHint {
+			panic(fmt.Sprintf("tool (%s) is incorrectly annotated as read-only", tool.Tool.Name))
+		}
+	}
 	if !t.readOnly {
 		t.writeTools = append(t.writeTools, tools...)
 	}
@@ -65,6 +70,15 @@ func (t *Toolset) AddWriteTools(tools ...server.ServerTool) *Toolset {
 }
 
 func (t *Toolset) AddReadTools(tools ...server.ServerTool) *Toolset {
+	for _, tool := range tools {
+		if !tool.Tool.Annotations.ReadOnlyHint {
+			panic(fmt.Sprintf("tool (%s) must be annotated as read-only", tool.Tool.Name))
+		}
+		tool.Tool.Annotations = mcp.ToolAnnotation{
+			ReadOnlyHint: true,
+			Title:        tool.Tool.Annotations.Title,
+		}
+	}
 	t.readTools = append(t.readTools, tools...)
 	return t
 }
diff --git a/third-party-licenses.darwin.md b/third-party-licenses.darwin.md
index 389bb96..cdb19b5 100644
--- a/third-party-licenses.darwin.md
+++ b/third-party-licenses.darwin.md
@@ -13,7 +13,7 @@ Some packages may only be included on certain architectures or operating systems
  - [github.com/google/go-github/v69/github](https://pkg.go.dev/github.com/google/go-github/v69/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v69.2.0/LICENSE))
  - [github.com/google/go-querystring/query](https://pkg.go.dev/github.com/google/go-querystring/query) ([BSD-3-Clause](https://github.com/google/go-querystring/blob/v1.1.0/LICENSE))
  - [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.6.0/LICENSE))
- - [github.com/mark3labs/mcp-go](https://pkg.go.dev/github.com/mark3labs/mcp-go) ([MIT](https://github.com/mark3labs/mcp-go/blob/v0.20.1/LICENSE))
+ - [github.com/mark3labs/mcp-go](https://pkg.go.dev/github.com/mark3labs/mcp-go) ([MIT](https://github.com/mark3labs/mcp-go/blob/v0.22.0/LICENSE))
  - [github.com/pelletier/go-toml/v2](https://pkg.go.dev/github.com/pelletier/go-toml/v2) ([MIT](https://github.com/pelletier/go-toml/blob/v2.2.3/LICENSE))
  - [github.com/sagikazarmark/locafero](https://pkg.go.dev/github.com/sagikazarmark/locafero) ([MIT](https://github.com/sagikazarmark/locafero/blob/v0.9.0/LICENSE))
  - [github.com/sirupsen/logrus](https://pkg.go.dev/github.com/sirupsen/logrus) ([MIT](https://github.com/sirupsen/logrus/blob/v1.9.3/LICENSE))
diff --git a/third-party-licenses.linux.md b/third-party-licenses.linux.md
index 389bb96..cdb19b5 100644
--- a/third-party-licenses.linux.md
+++ b/third-party-licenses.linux.md
@@ -13,7 +13,7 @@ Some packages may only be included on certain architectures or operating systems
  - [github.com/google/go-github/v69/github](https://pkg.go.dev/github.com/google/go-github/v69/github) ([BSD-3-Clause](https://github.com/google/go-github/blob/v69.2.0/LICENSE))
  - [github.com/google/go-querystring/query](https://pkg.go.dev/github.com/google/go-querystring/query) ([BSD-3-Clause](https://github.com/google/go-querystring/blob/v1.1.0/LICENSE))
  - [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.6.0/LICENSE))
- - [github.com/mark3labs/mcp-go](https://pkg.go.dev/github.com/mark3labs/mcp-go) ([MIT](https://github.com/mark3labs/mcp-go/blob/v0.20.1/LICENSE))
+ - [github.com/mark3labs/mcp-go](https://pkg.go.dev/github.com/mark3labs/mcp-go) ([MIT](https://github.com/mark3labs/mcp-go/blob/v0.22.0/LICENSE))
  - [github.com/pelletier/go-toml/v2](https://pkg.go.dev/github.com/pelletier/go-toml/v2) ([MIT](https://github.com/pelletier/go-toml/blob/v2.2.3/LICENSE))
  - [github.com/sagikazarmark/locafero](https://pkg.go.dev/github.com/sagikazarmark/locafero) ([MIT](https://github.com/sagikazarmark/locafero/blob/v0.9.0/LICENSE))
  - [github.com/sirupsen/logrus](https://pkg.go.dev/github.com/sirupsen/logrus) ([MIT](https://github.com/sirupsen/logrus/blob/v1.9.3/LICENSE))
diff --git a/third-party-licenses.windows.md b/third-party-licenses.windows.md
index 96d037c..b34d7e6 100644
--- a/third-party-licenses.windows.md
+++ b/third-party-licenses.windows.md
@@ -14,7 +14,7 @@ Some packages may only be included on certain architectures or operating systems
  - [github.com/google/go-querystring/query](https://pkg.go.dev/github.com/google/go-querystring/query) ([BSD-3-Clause](https://github.com/google/go-querystring/blob/v1.1.0/LICENSE))
  - [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.6.0/LICENSE))
  - [github.com/inconshreveable/mousetrap](https://pkg.go.dev/github.com/inconshreveable/mousetrap) ([Apache-2.0](https://github.com/inconshreveable/mousetrap/blob/v1.1.0/LICENSE))
- - [github.com/mark3labs/mcp-go](https://pkg.go.dev/github.com/mark3labs/mcp-go) ([MIT](https://github.com/mark3labs/mcp-go/blob/v0.20.1/LICENSE))
+ - [github.com/mark3labs/mcp-go](https://pkg.go.dev/github.com/mark3labs/mcp-go) ([MIT](https://github.com/mark3labs/mcp-go/blob/v0.22.0/LICENSE))
  - [github.com/pelletier/go-toml/v2](https://pkg.go.dev/github.com/pelletier/go-toml/v2) ([MIT](https://github.com/pelletier/go-toml/blob/v2.2.3/LICENSE))
  - [github.com/sagikazarmark/locafero](https://pkg.go.dev/github.com/sagikazarmark/locafero) ([MIT](https://github.com/sagikazarmark/locafero/blob/v0.9.0/LICENSE))
  - [github.com/sirupsen/logrus](https://pkg.go.dev/github.com/sirupsen/logrus) ([MIT](https://github.com/sirupsen/logrus/blob/v1.9.3/LICENSE))



Test patch:
diff --git a/conformance/conformance_test.go b/conformance/conformance_test.go
deleted file mode 100644
index cd69e01..0000000
--- a/conformance/conformance_test.go
+++ /dev/null
@@ -1,435 +0,0 @@
-//go:build conformance
-
-package conformance_test
-
-import (
-	"bufio"
-	"context"
-	"encoding/json"
-	"errors"
-	"fmt"
-	"io"
-	"os"
-	"reflect"
-	"strings"
-	"testing"
-
-	"github.com/docker/docker/api/types/container"
-	"github.com/docker/docker/api/types/network"
-	"github.com/docker/docker/client"
-	"github.com/docker/docker/pkg/stdcopy"
-	"github.com/google/go-cmp/cmp"
-	"github.com/google/go-cmp/cmp/cmpopts"
-	"github.com/stretchr/testify/require"
-)
-
-type maintainer string
-
-const (
-	anthropic maintainer = "anthropic"
-	github    maintainer = "github"
-)
-
-type testLogWriter struct {
-	t *testing.T
-}
-
-func (w testLogWriter) Write(p []byte) (n int, err error) {
-	w.t.Log(string(p))
-	return len(p), nil
-}
-
-func start(t *testing.T, m maintainer) server {
-	var image string
-	if m == github {
-		image = "github/github-mcp-server"
-	} else {
-		image = "mcp/github"
-	}
-
-	ctx := context.Background()
-	dockerClient, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
-	require.NoError(t, err)
-
-	containerCfg := &container.Config{
-		OpenStdin:    true,
-		AttachStdin:  true,
-		AttachStdout: true,
-		AttachStderr: true,
-		Env: []string{
-			fmt.Sprintf("GITHUB_PERSONAL_ACCESS_TOKEN=%s", os.Getenv("GITHUB_PERSONAL_ACCESS_TOKEN")),
-		},
-		Image: image,
-	}
-
-	resp, err := dockerClient.ContainerCreate(
-		ctx,
-		containerCfg,
-		&container.HostConfig{},
-		&network.NetworkingConfig{},
-		nil,
-		"")
-	require.NoError(t, err)
-
-	t.Cleanup(func() {
-		require.NoError(t, dockerClient.ContainerRemove(ctx, resp.ID, container.RemoveOptions{Force: true}))
-	})
-
-	hijackedResponse, err := dockerClient.ContainerAttach(ctx, resp.ID, container.AttachOptions{
-		Stream: true,
-		Stdin:  true,
-		Stdout: true,
-		Stderr: true,
-	})
-	require.NoError(t, err)
-	t.Cleanup(func() { hijackedResponse.Close() })
-
-	require.NoError(t, dockerClient.ContainerStart(ctx, resp.ID, container.StartOptions{}))
-
-	serverStart := make(chan serverStartResult)
-	go func() {
-		prOut, pwOut := io.Pipe()
-		prErr, pwErr := io.Pipe()
-
-		go func() {
-			// Ignore error, we should be done?
-			// TODO: maybe check for use of closed network connection specifically
-			_, _ = stdcopy.StdCopy(pwOut, pwErr, hijackedResponse.Reader)
-			pwOut.Close()
-			pwErr.Close()
-		}()
-
-		bufferedStderr := bufio.NewReader(prErr)
-		line, err := bufferedStderr.ReadString('\n')
-		if err != nil {
-			serverStart <- serverStartResult{err: err}
-		}
-
-		if strings.TrimSpace(line) != "GitHub MCP Server running on stdio" {
-			serverStart <- serverStartResult{
-				err: fmt.Errorf("unexpected server output: %s", line),
-			}
-			return
-		}
-
-		serverStart <- serverStartResult{
-			server: server{
-				m:      m,
-				log:    testLogWriter{t},
-				stdin:  hijackedResponse.Conn,
-				stdout: bufio.NewReader(prOut),
-			},
-		}
-	}()
-
-	t.Logf("waiting for %s server to start...", m)
-	serveResult := <-serverStart
-	require.NoError(t, serveResult.err, "expected the server to start successfully")
-
-	return serveResult.server
-}
-
-func TestCapabilities(t *testing.T) {
-	anthropicServer := start(t, anthropic)
-	githubServer := start(t, github)
-
-	req := initializeRequest{
-		JSONRPC: "2.0",
-		ID:      1,
-		Method:  "initialize",
-		Params: initializeParams{
-			ProtocolVersion: "2025-03-26",
-			Capabilities:    clientCapabilities{},
-			ClientInfo: clientInfo{
-				Name:    "ConformanceTest",
-				Version: "0.0.1",
-			},
-		},
-	}
-
-	require.NoError(t, anthropicServer.send(req))
-
-	var anthropicInitializeResponse initializeResponse
-	require.NoError(t, anthropicServer.receive(&anthropicInitializeResponse))
-
-	require.NoError(t, githubServer.send(req))
-
-	var ghInitializeResponse initializeResponse
-	require.NoError(t, githubServer.receive(&ghInitializeResponse))
-
-	// Any capabilities in the anthropic response should be present in the github response
-	// (though the github response may have additional capabilities)
-	if diff := diffNonNilFields(anthropicInitializeResponse.Result.Capabilities, ghInitializeResponse.Result.Capabilities, ""); diff != "" {
-		t.Errorf("capabilities mismatch:\n%s", diff)
-	}
-}
-
-func diffNonNilFields(a, b interface{}, path string) string {
-	var sb strings.Builder
-
-	va := reflect.ValueOf(a)
-	vb := reflect.ValueOf(b)
-
-	if !va.IsValid() {
-		return ""
-	}
-
-	if va.Kind() == reflect.Ptr {
-		if va.IsNil() {
-			return ""
-		}
-		if !vb.IsValid() || vb.IsNil() {
-			sb.WriteString(path + "\n")
-			return sb.String()
-		}
-		va = va.Elem()
-		vb = vb.Elem()
-	}
-
-	if va.Kind() != reflect.Struct || vb.Kind() != reflect.Struct {
-		return ""
-	}
-
-	t := va.Type()
-	for i := range va.NumField() {
-		field := t.Field(i)
-		if !field.IsExported() {
-			continue
-		}
-
-		subPath := field.Name
-		if path != "" {
-			subPath = fmt.Sprintf("%s.%s", path, field.Name)
-		}
-
-		fieldA := va.Field(i)
-		fieldB := vb.Field(i)
-
-		switch fieldA.Kind() {
-		case reflect.Ptr:
-			if fieldA.IsNil() {
-				continue // not required
-			}
-			if fieldB.IsNil() {
-				sb.WriteString(subPath + "\n")
-				continue
-			}
-			sb.WriteString(diffNonNilFields(fieldA.Interface(), fieldB.Interface(), subPath))
-
-		case reflect.Struct:
-			sb.WriteString(diffNonNilFields(fieldA.Interface(), fieldB.Interface(), subPath))
-
-		default:
-			zero := reflect.Zero(fieldA.Type())
-			if !reflect.DeepEqual(fieldA.Interface(), zero.Interface()) {
-				// fieldA is non-zero; now check that fieldB matches
-				if !reflect.DeepEqual(fieldA.Interface(), fieldB.Interface()) {
-					sb.WriteString(subPath + "\n")
-				}
-			}
-		}
-	}
-
-	return sb.String()
-}
-
-func TestListTools(t *testing.T) {
-	anthropicServer := start(t, anthropic)
-	githubServer := start(t, github)
-
-	req := listToolsRequest{
-		JSONRPC: "2.0",
-		ID:      1,
-		Method:  "tools/list",
-	}
-
-	require.NoError(t, anthropicServer.send(req))
-
-	var anthropicListToolsResponse listToolsResponse
-	require.NoError(t, anthropicServer.receive(&anthropicListToolsResponse))
-
-	require.NoError(t, githubServer.send(req))
-
-	var ghListToolsResponse listToolsResponse
-	require.NoError(t, githubServer.receive(&ghListToolsResponse))
-
-	require.NoError(t, isToolListSubset(anthropicListToolsResponse.Result, ghListToolsResponse.Result), "expected the github list tools response to be a subset of the anthropic list tools response")
-}
-
-func isToolListSubset(subset, superset listToolsResult) error {
-	// Build a map from tool name to Tool from the superset
-	supersetMap := make(map[string]tool)
-	for _, tool := range superset.Tools {
-		supersetMap[tool.Name] = tool
-	}
-
-	var err error
-	for _, tool := range subset.Tools {
-		sup, ok := supersetMap[tool.Name]
-		if !ok {
-			return fmt.Errorf("tool %q not found in superset", tool.Name)
-		}
-
-		// Intentionally ignore the description fields because there are lots of slight differences.
-		// if tool.Description != sup.Description {
-		// 	return fmt.Errorf("description mismatch for tool %q, got %q expected %q", tool.Name, tool.Description, sup.Description)
-		// }
-
-		// Ignore any description fields within the input schema properties for the same reason
-		ignoreDescOpt := cmp.FilterPath(func(p cmp.Path) bool {
-			// Look for a field named "Properties" somewhere in the path
-			for _, ps := range p {
-				if sf, ok := ps.(cmp.StructField); ok && sf.Name() == "Properties" {
-					return true
-				}
-			}
-			return false
-		}, cmpopts.IgnoreMapEntries(func(k string, _ any) bool {
-			return k == "description"
-		}))
-
-		if diff := cmp.Diff(tool.InputSchema, sup.InputSchema, ignoreDescOpt); diff != "" {
-			err = errors.Join(err, fmt.Errorf("inputSchema mismatch for tool %q:\n%s", tool.Name, diff))
-		}
-	}
-
-	return err
-}
-
-type serverStartResult struct {
-	server server
-	err    error
-}
-
-type server struct {
-	m   maintainer
-	log io.Writer
-
-	stdin  io.Writer
-	stdout *bufio.Reader
-}
-
-func (s server) send(req request) error {
-	b, err := req.marshal()
-	if err != nil {
-		return err
-	}
-
-	fmt.Fprintf(s.log, "sending %s: %s\n", s.m, string(b))
-
-	n, err := s.stdin.Write(append(b, '\n'))
-	if err != nil {
-		return err
-	}
-
-	if n != len(b)+1 {
-		return fmt.Errorf("wrote %d bytes, expected %d", n, len(b)+1)
-	}
-
-	return nil
-}
-
-func (s server) receive(res response) error {
-	line, err := s.stdout.ReadBytes('\n')
-	if err != nil {
-		if err == io.EOF {
-			return fmt.Errorf("EOF after reading %s", string(line))
-		}
-		return err
-	}
-
-	fmt.Fprintf(s.log, "received from %s: %s\n", s.m, string(line))
-
-	return res.unmarshal(line)
-}
-
-type request interface {
-	marshal() ([]byte, error)
-}
-
-type response interface {
-	unmarshal([]byte) error
-}
-
-type jsonRPRCRequest[params any] struct {
-	JSONRPC string `json:"jsonrpc"`
-	ID      int    `json:"id"`
-	Method  string `json:"method"`
-	Params  params `json:"params"`
-}
-
-func (r jsonRPRCRequest[any]) marshal() ([]byte, error) {
-	return json.Marshal(r)
-}
-
-type jsonRPRCResponse[result any] struct {
-	JSONRPC string `json:"jsonrpc"`
-	ID      int    `json:"id"`
-	Method  string `json:"method"`
-	Result  result `json:"result"`
-}
-
-func (r *jsonRPRCResponse[any]) unmarshal(b []byte) error {
-	return json.Unmarshal(b, r)
-}
-
-type initializeRequest = jsonRPRCRequest[initializeParams]
-
-type initializeParams struct {
-	ProtocolVersion string             `json:"protocolVersion"`
-	Capabilities    clientCapabilities `json:"capabilities"`
-	ClientInfo      clientInfo         `json:"clientInfo"`
-}
-
-type clientCapabilities struct{} // don't actually care about any of these right now
-
-type clientInfo struct {
-	Name    string `json:"name"`
-	Version string `json:"version"`
-}
-
-type initializeResponse = jsonRPRCResponse[initializeResult]
-
-type initializeResult struct {
-	ProtocolVersion string             `json:"protocolVersion"`
-	Capabilities    serverCapabilities `json:"capabilities"`
-	ServerInfo      serverInfo         `json:"serverInfo"`
-}
-
-type serverCapabilities struct {
-	Logging *struct{} `json:"logging,omitempty"`
-	Prompts *struct {
-		ListChanged bool `json:"listChanged,omitempty"`
-	} `json:"prompts,omitempty"`
-	Resources *struct {
-		Subscribe   bool `json:"subscribe,omitempty"`
-		ListChanged bool `json:"listChanged,omitempty"`
-	} `json:"resources,omitempty"`
-	Tools *struct {
-		ListChanged bool `json:"listChanged,omitempty"`
-	} `json:"tools,omitempty"`
-}
-
-type serverInfo struct {
-	Name    string `json:"name"`
-	Version string `json:"version"`
-}
-
-type listToolsRequest = jsonRPRCRequest[struct{}]
-
-type listToolsResponse = jsonRPRCResponse[listToolsResult]
-
-type listToolsResult struct {
-	Tools []tool `json:"tools"`
-}
-type tool struct {
-	Name        string      `json:"name"`
-	Description string      `json:"description,omitempty"`
-	InputSchema inputSchema `json:"inputSchema"`
-}
-
-type inputSchema struct {
-	Type       string         `json:"type"`
-	Properties map[string]any `json:"properties,omitempty"`
-	Required   []string       `json:"required,omitempty"`
-}
diff --git a/e2e/e2e_test.go b/e2e/e2e_test.go
new file mode 100644
index 0000000..3d8c45d
--- /dev/null
+++ b/e2e/e2e_test.go
@@ -0,0 +1,100 @@
+//go:build e2e
+
+package e2e_test
+
+import (
+	"context"
+	"encoding/json"
+	"os"
+	"os/exec"
+	"testing"
+	"time"
+
+	"github.com/google/go-github/v69/github"
+	mcpClient "github.com/mark3labs/mcp-go/client"
+	"github.com/mark3labs/mcp-go/mcp"
+	"github.com/stretchr/testify/require"
+)
+
+func TestE2E(t *testing.T) {
+	e2eServerToken := os.Getenv("GITHUB_MCP_SERVER_E2E_TOKEN")
+	if e2eServerToken == "" {
+		t.Fatalf("GITHUB_MCP_SERVER_E2E_TOKEN environment variable is not set")
+	}
+
+	// Build the Docker image for the MCP server.
+	buildDockerImage(t)
+
+	t.Setenv("GITHUB_PERSONAL_ACCESS_TOKEN", e2eServerToken) // The MCP Client merges the existing environment.
+	args := []string{
+		"docker",
+		"run",
+		"-i",
+		"--rm",
+		"-e",
+		"GITHUB_PERSONAL_ACCESS_TOKEN",
+		"github/e2e-github-mcp-server",
+	}
+	t.Log("Starting Stdio MCP client...")
+	client, err := mcpClient.NewStdioMCPClient(args[0], []string{}, args[1:]...)
+	require.NoError(t, err, "expected to create client successfully")
+
+	t.Run("Initialize", func(t *testing.T) {
+		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+		defer cancel()
+
+		request := mcp.InitializeRequest{}
+		request.Params.ProtocolVersion = "2025-03-26"
+		request.Params.ClientInfo = mcp.Implementation{
+			Name:    "e2e-test-client",
+			Version: "0.0.1",
+		}
+
+		result, err := client.Initialize(ctx, request)
+		require.NoError(t, err, "expected to initialize successfully")
+
+		require.Equal(t, "github-mcp-server", result.ServerInfo.Name)
+	})
+
+	t.Run("CallTool get_me", func(t *testing.T) {
+		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+		defer cancel()
+
+		// When we call the "get_me" tool
+		request := mcp.CallToolRequest{}
+		request.Params.Name = "get_me"
+
+		response, err := client.CallTool(ctx, request)
+		require.NoError(t, err, "expected to call 'get_me' tool successfully")
+
+		require.False(t, response.IsError, "expected result not to be an error")
+		require.Len(t, response.Content, 1, "expected content to have one item")
+
+		textContent, ok := response.Content[0].(mcp.TextContent)
+		require.True(t, ok, "expected content to be of type TextContent")
+
+		var trimmedContent struct {
+			Login string `json:"login"`
+		}
+		err = json.Unmarshal([]byte(textContent.Text), &trimmedContent)
+		require.NoError(t, err, "expected to unmarshal text content successfully")
+
+		// Then the login in the response should match the login obtained via the same
+		// token using the GitHub API.
+		client := github.NewClient(nil).WithAuthToken(e2eServerToken)
+		user, _, err := client.Users.Get(context.Background(), "")
+		require.NoError(t, err, "expected to get user successfully")
+		require.Equal(t, trimmedContent.Login, *user.Login, "expected login to match")
+	})
+
+	require.NoError(t, client.Close(), "expected to close client successfully")
+}
+
+func buildDockerImage(t *testing.T) {
+	t.Log("Building Docker image for e2e tests...")
+
+	cmd := exec.Command("docker", "build", "-t", "github/e2e-github-mcp-server", ".")
+	cmd.Dir = ".." // Run this in the context of the root, where the Dockerfile is located.
+	output, err := cmd.CombinedOutput()
+	require.NoError(t, err, "expected to build Docker image successfully, output: %s", string(output))
+}
diff --git a/pkg/github/code_scanning_test.go b/pkg/github/code_scanning_test.go
index c9895e2..40dabeb 100644
--- a/pkg/github/code_scanning_test.go
+++ b/pkg/github/code_scanning_test.go
@@ -127,6 +127,7 @@ func Test_ListCodeScanningAlerts(t *testing.T) {
 	assert.Contains(t, tool.InputSchema.Properties, "ref")
 	assert.Contains(t, tool.InputSchema.Properties, "state")
 	assert.Contains(t, tool.InputSchema.Properties, "severity")
+	assert.Contains(t, tool.InputSchema.Properties, "tool_name")
 	assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo"})
 
 	// Setup mock alerts for success case
@@ -159,20 +160,22 @@ func Test_ListCodeScanningAlerts(t *testing.T) {
 				mock.WithRequestMatchHandler(
 					mock.GetReposCodeScanningAlertsByOwnerByRepo,
 					expectQueryParams(t, map[string]string{
-						"ref":      "main",
-						"state":    "open",
-						"severity": "high",
+						"ref":       "main",
+						"state":     "open",
+						"severity":  "high",
+						"tool_name": "codeql",
 					}).andThen(
 						mockResponse(t, http.StatusOK, mockAlerts),
 					),
 				),
 			),
 			requestArgs: map[string]interface{}{
-				"owner":    "owner",
-				"repo":     "repo",
-				"ref":      "main",
-				"state":    "open",
-				"severity": "high",
+				"owner":     "owner",
+				"repo":      "repo",
+				"ref":       "main",
+				"state":     "open",
+				"severity":  "high",
+				"tool_name": "codeql",
 			},
 			expectError:    false,
 			expectedAlerts: mockAlerts,
diff --git a/pkg/github/secret_scanning_test.go b/pkg/github/secret_scanning_test.go
new file mode 100644
index 0000000..d32cbca
--- /dev/null
+++ b/pkg/github/secret_scanning_test.go
@@ -0,0 +1,243 @@
+package github
+
+import (
+	"context"
+	"encoding/json"
+	"net/http"
+	"testing"
+
+	"github.com/github/github-mcp-server/pkg/translations"
+	"github.com/google/go-github/v69/github"
+	"github.com/migueleliasweb/go-github-mock/src/mock"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func Test_GetSecretScanningAlert(t *testing.T) {
+	mockClient := github.NewClient(nil)
+	tool, _ := GetSecretScanningAlert(stubGetClientFn(mockClient), translations.NullTranslationHelper)
+
+	assert.Equal(t, "get_secret_scanning_alert", tool.Name)
+	assert.NotEmpty(t, tool.Description)
+	assert.Contains(t, tool.InputSchema.Properties, "owner")
+	assert.Contains(t, tool.InputSchema.Properties, "repo")
+	assert.Contains(t, tool.InputSchema.Properties, "alertNumber")
+	assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "alertNumber"})
+
+	// Setup mock alert for success case
+	mockAlert := &github.SecretScanningAlert{
+		Number:  github.Ptr(42),
+		State:   github.Ptr("open"),
+		HTMLURL: github.Ptr("https://github.com/owner/private-repo/security/secret-scanning/42"),
+	}
+
+	tests := []struct {
+		name           string
+		mockedClient   *http.Client
+		requestArgs    map[string]interface{}
+		expectError    bool
+		expectedAlert  *github.SecretScanningAlert
+		expectedErrMsg string
+	}{
+		{
+			name: "successful alert fetch",
+			mockedClient: mock.NewMockedHTTPClient(
+				mock.WithRequestMatch(
+					mock.GetReposSecretScanningAlertsByOwnerByRepoByAlertNumber,
+					mockAlert,
+				),
+			),
+			requestArgs: map[string]interface{}{
+				"owner":       "owner",
+				"repo":        "repo",
+				"alertNumber": float64(42),
+			},
+			expectError:   false,
+			expectedAlert: mockAlert,
+		},
+		{
+			name: "alert fetch fails",
+			mockedClient: mock.NewMockedHTTPClient(
+				mock.WithRequestMatchHandler(
+					mock.GetReposSecretScanningAlertsByOwnerByRepoByAlertNumber,
+					http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+						w.WriteHeader(http.StatusNotFound)
+						_, _ = w.Write([]byte(`{"message": "Not Found"}`))
+					}),
+				),
+			),
+			requestArgs: map[string]interface{}{
+				"owner":       "owner",
+				"repo":        "repo",
+				"alertNumber": float64(9999),
+			},
+			expectError:    true,
+			expectedErrMsg: "failed to get alert",
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			// Setup client with mock
+			client := github.NewClient(tc.mockedClient)
+			_, handler := GetSecretScanningAlert(stubGetClientFn(client), translations.NullTranslationHelper)
+
+			// Create call request
+			request := createMCPRequest(tc.requestArgs)
+
+			// Call handler
+			result, err := handler(context.Background(), request)
+
+			// Verify results
+			if tc.expectError {
+				require.Error(t, err)
+				assert.Contains(t, err.Error(), tc.expectedErrMsg)
+				return
+			}
+
+			require.NoError(t, err)
+
+			// Parse the result and get the text content if no error
+			textContent := getTextResult(t, result)
+
+			// Unmarshal and verify the result
+			var returnedAlert github.Alert
+			err = json.Unmarshal([]byte(textContent.Text), &returnedAlert)
+			assert.NoError(t, err)
+			assert.Equal(t, *tc.expectedAlert.Number, *returnedAlert.Number)
+			assert.Equal(t, *tc.expectedAlert.State, *returnedAlert.State)
+			assert.Equal(t, *tc.expectedAlert.HTMLURL, *returnedAlert.HTMLURL)
+
+		})
+	}
+}
+
+func Test_ListSecretScanningAlerts(t *testing.T) {
+	// Verify tool definition once
+	mockClient := github.NewClient(nil)
+	tool, _ := ListSecretScanningAlerts(stubGetClientFn(mockClient), translations.NullTranslationHelper)
+
+	assert.Equal(t, "list_secret_scanning_alerts", tool.Name)
+	assert.NotEmpty(t, tool.Description)
+	assert.Contains(t, tool.InputSchema.Properties, "owner")
+	assert.Contains(t, tool.InputSchema.Properties, "repo")
+	assert.Contains(t, tool.InputSchema.Properties, "state")
+	assert.Contains(t, tool.InputSchema.Properties, "secret_type")
+	assert.Contains(t, tool.InputSchema.Properties, "resolution")
+	assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo"})
+
+	// Setup mock alerts for success case
+	resolvedAlert := github.SecretScanningAlert{
+		Number:     github.Ptr(2),
+		HTMLURL:    github.Ptr("https://github.com/owner/private-repo/security/secret-scanning/2"),
+		State:      github.Ptr("resolved"),
+		Resolution: github.Ptr("false_positive"),
+		SecretType: github.Ptr("adafruit_io_key"),
+	}
+	openAlert := github.SecretScanningAlert{
+		Number:     github.Ptr(2),
+		HTMLURL:    github.Ptr("https://github.com/owner/private-repo/security/secret-scanning/3"),
+		State:      github.Ptr("open"),
+		Resolution: github.Ptr("false_positive"),
+		SecretType: github.Ptr("adafruit_io_key"),
+	}
+
+	tests := []struct {
+		name           string
+		mockedClient   *http.Client
+		requestArgs    map[string]interface{}
+		expectError    bool
+		expectedAlerts []*github.SecretScanningAlert
+		expectedErrMsg string
+	}{
+		{
+			name: "successful resolved alerts listing",
+			mockedClient: mock.NewMockedHTTPClient(
+				mock.WithRequestMatchHandler(
+					mock.GetReposSecretScanningAlertsByOwnerByRepo,
+					expectQueryParams(t, map[string]string{
+						"state": "resolved",
+					}).andThen(
+						mockResponse(t, http.StatusOK, []*github.SecretScanningAlert{&resolvedAlert}),
+					),
+				),
+			),
+			requestArgs: map[string]interface{}{
+				"owner": "owner",
+				"repo":  "repo",
+				"state": "resolved",
+			},
+			expectError:    false,
+			expectedAlerts: []*github.SecretScanningAlert{&resolvedAlert},
+		},
+		{
+			name: "successful alerts listing",
+			mockedClient: mock.NewMockedHTTPClient(
+				mock.WithRequestMatchHandler(
+					mock.GetReposSecretScanningAlertsByOwnerByRepo,
+					expectQueryParams(t, map[string]string{}).andThen(
+						mockResponse(t, http.StatusOK, []*github.SecretScanningAlert{&resolvedAlert, &openAlert}),
+					),
+				),
+			),
+			requestArgs: map[string]interface{}{
+				"owner": "owner",
+				"repo":  "repo",
+			},
+			expectError:    false,
+			expectedAlerts: []*github.SecretScanningAlert{&resolvedAlert, &openAlert},
+		},
+		{
+			name: "alerts listing fails",
+			mockedClient: mock.NewMockedHTTPClient(
+				mock.WithRequestMatchHandler(
+					mock.GetReposSecretScanningAlertsByOwnerByRepo,
+					http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+						w.WriteHeader(http.StatusUnauthorized)
+						_, _ = w.Write([]byte(`{"message": "Unauthorized access"}`))
+					}),
+				),
+			),
+			requestArgs: map[string]interface{}{
+				"owner": "owner",
+				"repo":  "repo",
+			},
+			expectError:    true,
+			expectedErrMsg: "failed to list alerts",
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			client := github.NewClient(tc.mockedClient)
+			_, handler := ListSecretScanningAlerts(stubGetClientFn(client), translations.NullTranslationHelper)
+
+			request := createMCPRequest(tc.requestArgs)
+
+			result, err := handler(context.Background(), request)
+
+			if tc.expectError {
+				require.Error(t, err)
+				assert.Contains(t, err.Error(), tc.expectedErrMsg)
+				return
+			}
+
+			require.NoError(t, err)
+
+			textContent := getTextResult(t, result)
+
+			// Unmarshal and verify the result
+			var returnedAlerts []*github.SecretScanningAlert
+			err = json.Unmarshal([]byte(textContent.Text), &returnedAlerts)
+			assert.NoError(t, err)
+			assert.Len(t, returnedAlerts, len(tc.expectedAlerts))
+			for i, alert := range returnedAlerts {
+				assert.Equal(t, *tc.expectedAlerts[i].Number, *alert.Number)
+				assert.Equal(t, *tc.expectedAlerts[i].HTMLURL, *alert.HTMLURL)
+				assert.Equal(t, *tc.expectedAlerts[i].State, *alert.State)
+				assert.Equal(t, *tc.expectedAlerts[i].Resolution, *alert.Resolution)
+				assert.Equal(t, *tc.expectedAlerts[i].SecretType, *alert.SecretType)
+			}
+		})
+	}
+}