From 2d548c63b64180a3ad4611cf2e7b746738e7d079 Mon Sep 17 00:00:00 2001 From: "avi@robusta.dev" Date: Sun, 8 Mar 2026 20:45:20 +0200 Subject: [PATCH 1/4] updated github mcp Signed-off-by: avi@robusta.dev --- .../builtin-toolsets/github-mcp.md | 203 +++++------------- helm/holmes/templates/holmes.yaml | 17 ++ .../mcp-servers/github/deployment.yaml | 4 +- .../mcp-servers/github/networkpolicy.yaml | 2 +- helm/holmes/templates/toolset-config.yaml | 15 +- helm/holmes/values.yaml | 32 ++- 6 files changed, 101 insertions(+), 172 deletions(-) diff --git a/docs/data-sources/builtin-toolsets/github-mcp.md b/docs/data-sources/builtin-toolsets/github-mcp.md index 84899b0c05..8bae7e00ee 100644 --- a/docs/data-sources/builtin-toolsets/github-mcp.md +++ b/docs/data-sources/builtin-toolsets/github-mcp.md @@ -4,9 +4,12 @@ The GitHub MCP server provides access to GitHub repositories, pull requests, iss ## Overview -The GitHub MCP server is deployed as a separate pod in your cluster when using the Holmes or Robusta Helm charts. For CLI users, you'll need to deploy the MCP server manually and configure Holmes to connect to it. +Holmes supports two authentication methods for GitHub: -The server supports both GitHub.com and GitHub Enterprise Server, making it suitable for both cloud and on-premises deployments. +- **Personal Access Token (PAT)**: A self-hosted MCP server pod is deployed in your cluster to proxy requests to the GitHub API. +- **GitHub App**: Holmes connects directly to [GitHub's official MCP server](https://github.com/github/github-mcp-server) remote endpoint. No self-hosted pod is needed. + +Both methods support GitHub.com and GitHub Enterprise Server. ## Prerequisites @@ -259,7 +262,7 @@ Before deploying the GitHub MCP server, you need a GitHub Personal Access Token ### Using a GitHub App -Instead of a Personal Access Token, you can authenticate using a [GitHub App](https://docs.github.com/en/apps/creating-github-apps/about-creating-github-apps/about-creating-github-apps). This method uses GitHub's remote MCP endpoint (`api.githubcopilot.com/mcp`) with automatically refreshed installation tokens. +Instead of a Personal Access Token, you can authenticate using a [GitHub App](https://docs.github.com/en/apps/creating-github-apps/about-creating-github-apps/about-creating-github-apps). This method connects to [GitHub's official MCP server](https://github.com/github/github-mcp-server) remote endpoint with automatically refreshed installation tokens. No self-hosted MCP server pod is needed. **Step 1: Create a GitHub App** @@ -296,14 +299,7 @@ Note the **Installation ID** from the URL after installation: `https://github.co Find the **App ID** on the App's settings page (under "About"). -**Step 5: Configure the GitHub MCP Server** - -Choose one of: - -- **Self-hosted in cluster** — Deploy the MCP server using [Steps 1–2 from the PAT section above](#using-a-personal-access-token), then use its in-cluster URL. -- **GitHub's remote endpoint** — Use `https://api.githubcopilot.com/mcp` (no deployment needed). - -**Step 6: Configure Holmes** +**Step 5: Configure Holmes** === "Holmes CLI" @@ -315,31 +311,16 @@ Choose one of: export GITHUB_APP_PRIVATE_KEY="$(cat /path/to/private-key.pem)" ``` - === "Self-hosted MCP Server" - - ```yaml - mcp_servers: - github: - description: "GitHub MCP Server" - config: - url: "http://github-mcp-server.holmes-mcp.svc.cluster.local:8000/sse" - mode: "sse" - extra_headers: - Authorization: "Bearer {{ env.AUTO_GENERATED_GITHUB_TOKEN }}" - ``` - - === "GitHub's Remote MCP" - - ```yaml - mcp_servers: - github: - description: "GitHub MCP Server" - config: - url: "https://api.githubcopilot.com/mcp" - mode: "streamable-http" - extra_headers: - Authorization: "Bearer {{ env.AUTO_GENERATED_GITHUB_TOKEN }}" - ``` + ```yaml + mcp_servers: + github: + description: "GitHub MCP Server" + config: + url: "https://api.githubcopilot.com/mcp" + mode: "streamable-http" + extra_headers: + Authorization: "Bearer {{ env.AUTO_GENERATED_GITHUB_TOKEN }}" + ``` Holmes will automatically generate an installation token at startup and refresh it in the background. @@ -357,65 +338,27 @@ Choose one of: **Add to your `values.yaml`:** - === "Self-hosted MCP Server" + ```yaml + mcpAddons: + github: + enabled: true + auth: + githubApp: + secretName: "holmes-github-app" + ``` - ```yaml - additionalEnvVars: - - name: GITHUB_APP_ID - valueFrom: - secretKeyRef: - name: holmes-github-app - key: GITHUB_APP_ID - - name: GITHUB_APP_INSTALLATION_ID - valueFrom: - secretKeyRef: - name: holmes-github-app - key: GITHUB_APP_INSTALLATION_ID - - name: GITHUB_APP_PRIVATE_KEY - valueFrom: - secretKeyRef: - name: holmes-github-app - key: GITHUB_APP_PRIVATE_KEY - - mcp_servers: - github: - description: "GitHub MCP Server" - config: - url: "http://github-mcp-server.holmes-mcp.svc.cluster.local:8000/sse" - mode: "sse" - extra_headers: - Authorization: "Bearer {{ env.AUTO_GENERATED_GITHUB_TOKEN }}" - ``` - - === "GitHub's Remote MCP" - - ```yaml - additionalEnvVars: - - name: GITHUB_APP_ID - valueFrom: - secretKeyRef: - name: holmes-github-app - key: GITHUB_APP_ID - - name: GITHUB_APP_INSTALLATION_ID - valueFrom: - secretKeyRef: - name: holmes-github-app - key: GITHUB_APP_INSTALLATION_ID - - name: GITHUB_APP_PRIVATE_KEY - valueFrom: - secretKeyRef: - name: holmes-github-app - key: GITHUB_APP_PRIVATE_KEY - - mcp_servers: - github: - description: "GitHub MCP Server" - config: - url: "https://api.githubcopilot.com/mcp" - mode: "streamable-http" - extra_headers: - Authorization: "Bearer {{ env.AUTO_GENERATED_GITHUB_TOKEN }}" - ``` + No self-hosted MCP server pod is deployed. Holmes connects directly to GitHub's official MCP endpoint. To use a different endpoint, set `auth.githubApp.url` and `auth.githubApp.mode`: + + ```yaml + mcpAddons: + github: + enabled: true + auth: + githubApp: + secretName: "holmes-github-app" + url: "http://my-custom-mcp-server:8000/sse" # default: https://api.githubcopilot.com/mcp + mode: "sse" # default: streamable-http + ``` ```bash helm upgrade --install holmes robusta/holmes -f values.yaml @@ -435,67 +378,17 @@ Choose one of: **Add to your `generated_values.yaml`:** - === "Self-hosted MCP Server" - - ```yaml - holmes: - additionalEnvVars: - - name: GITHUB_APP_ID - valueFrom: - secretKeyRef: - name: holmes-github-app - key: GITHUB_APP_ID - - name: GITHUB_APP_INSTALLATION_ID - valueFrom: - secretKeyRef: - name: holmes-github-app - key: GITHUB_APP_INSTALLATION_ID - - name: GITHUB_APP_PRIVATE_KEY - valueFrom: - secretKeyRef: - name: holmes-github-app - key: GITHUB_APP_PRIVATE_KEY - - mcp_servers: - github: - description: "GitHub MCP Server" - config: - url: "http://github-mcp-server.holmes-mcp.svc.cluster.local:8000/sse" - mode: "sse" - extra_headers: - Authorization: "Bearer {{ env.AUTO_GENERATED_GITHUB_TOKEN }}" - ``` - - === "GitHub's Remote MCP" - - ```yaml - holmes: - additionalEnvVars: - - name: GITHUB_APP_ID - valueFrom: - secretKeyRef: - name: holmes-github-app - key: GITHUB_APP_ID - - name: GITHUB_APP_INSTALLATION_ID - valueFrom: - secretKeyRef: - name: holmes-github-app - key: GITHUB_APP_INSTALLATION_ID - - name: GITHUB_APP_PRIVATE_KEY - valueFrom: - secretKeyRef: - name: holmes-github-app - key: GITHUB_APP_PRIVATE_KEY - - mcp_servers: - github: - description: "GitHub MCP Server" - config: - url: "https://api.githubcopilot.com/mcp" - mode: "streamable-http" - extra_headers: - Authorization: "Bearer {{ env.AUTO_GENERATED_GITHUB_TOKEN }}" - ``` + ```yaml + holmes: + mcpAddons: + github: + enabled: true + auth: + githubApp: + secretName: "holmes-github-app" + ``` + + No self-hosted MCP server pod is deployed. Holmes connects directly to GitHub's official MCP endpoint. To override the URL, set `auth.githubApp.url` and `auth.githubApp.mode`. ```bash helm upgrade --install robusta robusta/robusta -f generated_values.yaml --set clusterName=YOUR_CLUSTER_NAME diff --git a/helm/holmes/templates/holmes.yaml b/helm/holmes/templates/holmes.yaml index ba23e4e601..85185d6668 100644 --- a/helm/holmes/templates/holmes.yaml +++ b/helm/holmes/templates/holmes.yaml @@ -83,6 +83,23 @@ spec: - name: IS_OPENSHIFT value: "True" {{- end }} + {{- if and .Values.mcpAddons.github.enabled .Values.mcpAddons.github.auth.githubApp.secretName }} + - name: GITHUB_APP_ID + valueFrom: + secretKeyRef: + name: {{ .Values.mcpAddons.github.auth.githubApp.secretName }} + key: GITHUB_APP_ID + - name: GITHUB_APP_INSTALLATION_ID + valueFrom: + secretKeyRef: + name: {{ .Values.mcpAddons.github.auth.githubApp.secretName }} + key: GITHUB_APP_INSTALLATION_ID + - name: GITHUB_APP_PRIVATE_KEY + valueFrom: + secretKeyRef: + name: {{ .Values.mcpAddons.github.auth.githubApp.secretName }} + key: GITHUB_APP_PRIVATE_KEY + {{- end }} {{- if .Values.additionalEnvVars -}} {{ toYaml .Values.additionalEnvVars | nindent 10 }} {{- end }} diff --git a/helm/holmes/templates/mcp-servers/github/deployment.yaml b/helm/holmes/templates/mcp-servers/github/deployment.yaml index bd3683374c..be6095eea1 100644 --- a/helm/holmes/templates/mcp-servers/github/deployment.yaml +++ b/helm/holmes/templates/mcp-servers/github/deployment.yaml @@ -1,4 +1,4 @@ -{{- if .Values.mcpAddons.github.enabled }} +{{- if and .Values.mcpAddons.github.enabled (not .Values.mcpAddons.github.auth.githubApp.secretName) }} --- apiVersion: v1 kind: ConfigMap @@ -77,7 +77,7 @@ spec: - name: GITHUB_PERSONAL_ACCESS_TOKEN valueFrom: secretKeyRef: - name: {{ required "mcpAddons.github.auth.secretName is required" .Values.mcpAddons.github.auth.secretName }} + name: {{ required "mcpAddons.github.auth.secretName is required for PAT auth" .Values.mcpAddons.github.auth.secretName }} key: {{ .Values.mcpAddons.github.auth.secretKey | default "token" }} {{- if .Values.mcpAddons.github.config.toolsets }} - name: GITHUB_TOOLSETS diff --git a/helm/holmes/templates/mcp-servers/github/networkpolicy.yaml b/helm/holmes/templates/mcp-servers/github/networkpolicy.yaml index c71ef4eb02..95a9501a17 100644 --- a/helm/holmes/templates/mcp-servers/github/networkpolicy.yaml +++ b/helm/holmes/templates/mcp-servers/github/networkpolicy.yaml @@ -1,4 +1,4 @@ -{{- if and .Values.mcpAddons.github.enabled .Values.mcpAddons.github.networkPolicy.enabled }} +{{- if and .Values.mcpAddons.github.enabled (not .Values.mcpAddons.github.auth.githubApp.secretName) .Values.mcpAddons.github.networkPolicy.enabled }} apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: diff --git a/helm/holmes/templates/toolset-config.yaml b/helm/holmes/templates/toolset-config.yaml index c853c02a73..552292565c 100644 --- a/helm/holmes/templates/toolset-config.yaml +++ b/helm/holmes/templates/toolset-config.yaml @@ -88,13 +88,18 @@ data: {{- $mcpServers = merge $mcpServers $gcpMcpServers }} {{- end }} {{- if .Values.mcpAddons.github.enabled }} + {{- $githubConfig := dict "icon_url" "https://cdn.simpleicons.org/github/181717" }} + {{- if .Values.mcpAddons.github.auth.githubApp.secretName }} + {{- $_ := set $githubConfig "url" .Values.mcpAddons.github.auth.githubApp.url }} + {{- $_ := set $githubConfig "mode" .Values.mcpAddons.github.auth.githubApp.mode }} + {{- $_ := set $githubConfig "extra_headers" (dict "Authorization" "Bearer {{ env.AUTO_GENERATED_GITHUB_TOKEN }}") }} + {{- else }} + {{- $_ := set $githubConfig "url" (printf "http://%s-github-mcp-server.%s.svc.cluster.local:8000/sse" .Release.Name .Release.Namespace) }} + {{- $_ := set $githubConfig "mode" "sse" }} + {{- end }} {{- $githubMcpServers := dict "github" (dict "description" "GitHub MCP Server - access repositories, pull requests, issues, and GitHub Actions. Debug CI failures, search code, and delegate tasks to Copilot." - "config" (dict - "url" (printf "http://%s-github-mcp-server.%s.svc.cluster.local:8000/sse" .Release.Name .Release.Namespace) - "mode" "sse" - "icon_url" "https://cdn.simpleicons.org/github/181717" - ) + "config" $githubConfig "llm_instructions" (include "holmes.githubMcp.llmInstructions" . | trim) ) }} diff --git a/helm/holmes/values.yaml b/helm/holmes/values.yaml index 4af7f9fd55..ab13932fd3 100644 --- a/helm/holmes/values.yaml +++ b/helm/holmes/values.yaml @@ -323,19 +323,17 @@ mcpAddons: # GitHub MCP Server Configuration # Provides access to GitHub repositories, pull requests, issues, and GitHub Actions - # Uses a prebuilt image that bundles GitHub MCP server with Supergateway for HTTP transport # - # AUTHENTICATION: - # Create a GitHub Personal Access Token (PAT) with appropriate permissions: - # - repo: Full control of private repositories (or public_repo for public only) - # - workflow: Update GitHub Action workflows (for CI/CD debugging) - # Then create a Kubernetes secret: + # AUTHENTICATION (choose one): + # Option 1 - PAT: Deploys a self-hosted MCP server pod using a prebuilt image # kubectl create secret generic github-mcp-token --from-literal=token= -n + # Option 2 - GitHub App: Connects to GitHub's official remote MCP endpoint (no pod deployed) + # kubectl create secret generic github-app-secret --from-literal=GITHUB_APP_ID=... -n # # GITHUB ENTERPRISE: # Set config.host to your GitHub Enterprise hostname (e.g., "github.mycompany.com") # - # CUSTOM IMAGE (optional): + # CUSTOM IMAGE (PAT only): # To build your own image, see: https://github.com/robusta-dev/holmes-mcp-integrations/tree/master/servers/github github: enabled: false @@ -344,11 +342,27 @@ mcpAddons: registry: "us-central1-docker.pkg.dev/genuine-flight-317411/mcp" imagePullPolicy: IfNotPresent - # Authentication - PAT from Kubernetes secret + # Authentication - choose ONE of the two methods below auth: - secretName: "" # Required: name of K8s secret containing the GitHub PAT + # Option 1: Personal Access Token (PAT) + # Create a secret: kubectl create secret generic github-mcp-token --from-literal=token= -n + secretName: "" # Name of K8s secret containing the GitHub PAT secretKey: "token" # Key in secret (default: "token") + # Option 2: GitHub App installation token + # Creates short-lived tokens from GitHub App credentials. Holmes auto-generates + # and refreshes tokens at runtime. No self-hosted MCP server pod is deployed; + # Holmes connects directly to GitHub's official remote MCP endpoint. + # kubectl create secret generic github-app-secret \ + # --from-literal=GITHUB_APP_ID= \ + # --from-literal=GITHUB_APP_INSTALLATION_ID= \ + # --from-file=GITHUB_APP_PRIVATE_KEY=/path/to/private-key.pem \ + # -n + githubApp: + secretName: "" # Name of K8s secret containing GitHub App credentials + url: "https://api.githubcopilot.com/mcp" # MCP endpoint URL (override for custom MCP servers) + mode: "streamable-http" # MCP transport mode (override for custom MCP servers) + config: # GitHub Enterprise hostname (leave empty for github.com) # Example: "https://github.mycompany.com" From a83855fcbec55ae02b6d3c85ecc03832c0c429c4 Mon Sep 17 00:00:00 2001 From: "avi@robusta.dev" Date: Mon, 9 Mar 2026 09:42:24 +0200 Subject: [PATCH 2/4] updated github mcp Signed-off-by: avi@robusta.dev --- .../builtin-toolsets/github-mcp.md | 35 +++++++------------ helm/holmes/templates/holmes.yaml | 17 --------- .../mcp-servers/github/deployment.yaml | 32 ++++++++++++++++- .../mcp-servers/github/networkpolicy.yaml | 2 +- helm/holmes/templates/toolset-config.yaml | 25 +++++++++---- helm/holmes/values.yaml | 16 ++++----- 6 files changed, 70 insertions(+), 57 deletions(-) diff --git a/docs/data-sources/builtin-toolsets/github-mcp.md b/docs/data-sources/builtin-toolsets/github-mcp.md index 8bae7e00ee..c09e810a99 100644 --- a/docs/data-sources/builtin-toolsets/github-mcp.md +++ b/docs/data-sources/builtin-toolsets/github-mcp.md @@ -4,10 +4,10 @@ The GitHub MCP server provides access to GitHub repositories, pull requests, iss ## Overview -Holmes supports two authentication methods for GitHub: +Holmes supports two authentication methods for GitHub. Both deploy a self-hosted MCP server pod in your cluster that wraps the [official GitHub MCP server](https://github.com/github/github-mcp-server): -- **Personal Access Token (PAT)**: A self-hosted MCP server pod is deployed in your cluster to proxy requests to the GitHub API. -- **GitHub App**: Holmes connects directly to [GitHub's official MCP server](https://github.com/github/github-mcp-server) remote endpoint. No self-hosted pod is needed. +- **Personal Access Token (PAT)**: Uses the standard `github-mcp` image. The PAT is passed directly to the MCP server. +- **GitHub App**: Uses the `github-app-mcp` image which automatically generates and refreshes short-lived installation tokens from GitHub App credentials. Both methods support GitHub.com and GitHub Enterprise Server. @@ -97,7 +97,7 @@ Before deploying the GitHub MCP server, you need a GitHub Personal Access Token spec: containers: - name: github-mcp - image: me-west1-docker.pkg.dev/robusta-development/development/github-mcp:1.0.0 + image: us-central1-docker.pkg.dev/genuine-flight-317411/mcp/github-mcp:1.0.1 imagePullPolicy: IfNotPresent ports: - containerPort: 8000 @@ -262,7 +262,7 @@ Before deploying the GitHub MCP server, you need a GitHub Personal Access Token ### Using a GitHub App -Instead of a Personal Access Token, you can authenticate using a [GitHub App](https://docs.github.com/en/apps/creating-github-apps/about-creating-github-apps/about-creating-github-apps). This method connects to [GitHub's official MCP server](https://github.com/github/github-mcp-server) remote endpoint with automatically refreshed installation tokens. No self-hosted MCP server pod is needed. +Instead of a Personal Access Token, you can authenticate using a [GitHub App](https://docs.github.com/en/apps/creating-github-apps/about-creating-github-apps/about-creating-github-apps). This deploys the `github-app-mcp` image which wraps the official GitHub MCP server with automatic installation token generation and refresh. **Step 1: Create a GitHub App** @@ -347,18 +347,7 @@ Find the **App ID** on the App's settings page (under "About"). secretName: "holmes-github-app" ``` - No self-hosted MCP server pod is deployed. Holmes connects directly to GitHub's official MCP endpoint. To use a different endpoint, set `auth.githubApp.url` and `auth.githubApp.mode`: - - ```yaml - mcpAddons: - github: - enabled: true - auth: - githubApp: - secretName: "holmes-github-app" - url: "http://my-custom-mcp-server:8000/sse" # default: https://api.githubcopilot.com/mcp - mode: "sse" # default: streamable-http - ``` + A self-hosted MCP server pod is deployed using the `github-app-mcp` image, which generates and auto-refreshes installation tokens internally. The token refresh interval defaults to 30 minutes. ```bash helm upgrade --install holmes robusta/holmes -f values.yaml @@ -388,21 +377,21 @@ Find the **App ID** on the App's settings page (under "About"). secretName: "holmes-github-app" ``` - No self-hosted MCP server pod is deployed. Holmes connects directly to GitHub's official MCP endpoint. To override the URL, set `auth.githubApp.url` and `auth.githubApp.mode`. + A self-hosted MCP server pod is deployed using the `github-app-mcp` image, which generates and auto-refreshes installation tokens internally. ```bash helm upgrade --install robusta robusta/robusta -f generated_values.yaml --set clusterName=YOUR_CLUSTER_NAME ``` !!! info "How token refresh works" - When `GITHUB_APP_ID`, `GITHUB_APP_INSTALLATION_ID`, and `GITHUB_APP_PRIVATE_KEY` are set, Holmes automatically: + The `github-app-mcp` image handles token management internally: - 1. Generates a JWT signed with the private key + 1. At startup, generates a JWT signed with the private key 2. Exchanges it for a short-lived GitHub installation token - 3. Sets `AUTO_GENERATED_GITHUB_TOKEN` in the environment - 4. Runs a background thread that refreshes the token before it expires + 3. Sets the token as `GITHUB_PERSONAL_ACCESS_TOKEN` for the underlying MCP server + 4. A background thread refreshes the token every 30 minutes - The refresh interval defaults to 30 minutes. Override with `GITHUB_APP_TOKEN_REFRESH_INTERVAL_SECONDS`. + For CLI usage, Holmes handles token refresh in-process using `GITHUB_APP_ID`, `GITHUB_APP_INSTALLATION_ID`, and `GITHUB_APP_PRIVATE_KEY` environment variables. ## Available Tools diff --git a/helm/holmes/templates/holmes.yaml b/helm/holmes/templates/holmes.yaml index 85185d6668..ba23e4e601 100644 --- a/helm/holmes/templates/holmes.yaml +++ b/helm/holmes/templates/holmes.yaml @@ -83,23 +83,6 @@ spec: - name: IS_OPENSHIFT value: "True" {{- end }} - {{- if and .Values.mcpAddons.github.enabled .Values.mcpAddons.github.auth.githubApp.secretName }} - - name: GITHUB_APP_ID - valueFrom: - secretKeyRef: - name: {{ .Values.mcpAddons.github.auth.githubApp.secretName }} - key: GITHUB_APP_ID - - name: GITHUB_APP_INSTALLATION_ID - valueFrom: - secretKeyRef: - name: {{ .Values.mcpAddons.github.auth.githubApp.secretName }} - key: GITHUB_APP_INSTALLATION_ID - - name: GITHUB_APP_PRIVATE_KEY - valueFrom: - secretKeyRef: - name: {{ .Values.mcpAddons.github.auth.githubApp.secretName }} - key: GITHUB_APP_PRIVATE_KEY - {{- end }} {{- if .Values.additionalEnvVars -}} {{ toYaml .Values.additionalEnvVars | nindent 10 }} {{- end }} diff --git a/helm/holmes/templates/mcp-servers/github/deployment.yaml b/helm/holmes/templates/mcp-servers/github/deployment.yaml index be6095eea1..a90ff99c43 100644 --- a/helm/holmes/templates/mcp-servers/github/deployment.yaml +++ b/helm/holmes/templates/mcp-servers/github/deployment.yaml @@ -1,4 +1,4 @@ -{{- if and .Values.mcpAddons.github.enabled (not .Values.mcpAddons.github.auth.githubApp.secretName) }} +{{- if .Values.mcpAddons.github.enabled }} --- apiVersion: v1 kind: ConfigMap @@ -62,7 +62,11 @@ spec: {{- end }} containers: - name: github-mcp + {{- if .Values.mcpAddons.github.auth.githubApp.secretName }} + image: "{{ .Values.mcpAddons.github.auth.githubApp.registry }}/{{ .Values.mcpAddons.github.auth.githubApp.image }}" + {{- else }} image: "{{ .Values.mcpAddons.github.registry }}/{{ .Values.mcpAddons.github.image }}" + {{- end }} imagePullPolicy: {{ .Values.mcpAddons.github.imagePullPolicy | default "IfNotPresent" }} ports: - containerPort: 8000 @@ -70,15 +74,41 @@ spec: protocol: TCP args: - "--stdio" + {{- if .Values.mcpAddons.github.auth.githubApp.secretName }} + - "python3 /app/wrapper.py" + {{- else }} - "github-mcp-server stdio" + {{- end }} - "--port" - "8000" + {{- if .Values.mcpAddons.github.auth.githubApp.secretName }} + - "--outputTransport" + - "streamableHttp" + {{- end }} env: + {{- if .Values.mcpAddons.github.auth.githubApp.secretName }} + - name: GITHUB_APP_ID + valueFrom: + secretKeyRef: + name: {{ .Values.mcpAddons.github.auth.githubApp.secretName }} + key: GITHUB_APP_ID + - name: GITHUB_APP_INSTALLATION_ID + valueFrom: + secretKeyRef: + name: {{ .Values.mcpAddons.github.auth.githubApp.secretName }} + key: GITHUB_APP_INSTALLATION_ID + - name: GITHUB_APP_PRIVATE_KEY + valueFrom: + secretKeyRef: + name: {{ .Values.mcpAddons.github.auth.githubApp.secretName }} + key: GITHUB_APP_PRIVATE_KEY + {{- else }} - name: GITHUB_PERSONAL_ACCESS_TOKEN valueFrom: secretKeyRef: name: {{ required "mcpAddons.github.auth.secretName is required for PAT auth" .Values.mcpAddons.github.auth.secretName }} key: {{ .Values.mcpAddons.github.auth.secretKey | default "token" }} + {{- end }} {{- if .Values.mcpAddons.github.config.toolsets }} - name: GITHUB_TOOLSETS valueFrom: diff --git a/helm/holmes/templates/mcp-servers/github/networkpolicy.yaml b/helm/holmes/templates/mcp-servers/github/networkpolicy.yaml index 95a9501a17..c71ef4eb02 100644 --- a/helm/holmes/templates/mcp-servers/github/networkpolicy.yaml +++ b/helm/holmes/templates/mcp-servers/github/networkpolicy.yaml @@ -1,4 +1,4 @@ -{{- if and .Values.mcpAddons.github.enabled (not .Values.mcpAddons.github.auth.githubApp.secretName) .Values.mcpAddons.github.networkPolicy.enabled }} +{{- if and .Values.mcpAddons.github.enabled .Values.mcpAddons.github.networkPolicy.enabled }} apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: diff --git a/helm/holmes/templates/toolset-config.yaml b/helm/holmes/templates/toolset-config.yaml index 552292565c..6f15f70237 100644 --- a/helm/holmes/templates/toolset-config.yaml +++ b/helm/holmes/templates/toolset-config.yaml @@ -88,15 +88,25 @@ data: {{- $mcpServers = merge $mcpServers $gcpMcpServers }} {{- end }} {{- if .Values.mcpAddons.github.enabled }} - {{- $githubConfig := dict "icon_url" "https://cdn.simpleicons.org/github/181717" }} {{- if .Values.mcpAddons.github.auth.githubApp.secretName }} - {{- $_ := set $githubConfig "url" .Values.mcpAddons.github.auth.githubApp.url }} - {{- $_ := set $githubConfig "mode" .Values.mcpAddons.github.auth.githubApp.mode }} - {{- $_ := set $githubConfig "extra_headers" (dict "Authorization" "Bearer {{ env.AUTO_GENERATED_GITHUB_TOKEN }}") }} + {{- $githubConfig := dict + "url" (printf "http://%s-github-mcp-server.%s.svc.cluster.local:8000/mcp" .Release.Name .Release.Namespace) + "mode" "streamable-http" + "icon_url" "https://cdn.simpleicons.org/github/181717" + }} + {{- $githubMcpServers := dict "github" (dict + "description" "GitHub MCP Server - access repositories, pull requests, issues, and GitHub Actions. Debug CI failures, search code, and delegate tasks to Copilot." + "config" $githubConfig + "llm_instructions" (include "holmes.githubMcp.llmInstructions" . | trim) + ) + }} + {{- $mcpServers = merge $mcpServers $githubMcpServers }} {{- else }} - {{- $_ := set $githubConfig "url" (printf "http://%s-github-mcp-server.%s.svc.cluster.local:8000/sse" .Release.Name .Release.Namespace) }} - {{- $_ := set $githubConfig "mode" "sse" }} - {{- end }} + {{- $githubConfig := dict + "url" (printf "http://%s-github-mcp-server.%s.svc.cluster.local:8000/sse" .Release.Name .Release.Namespace) + "mode" "sse" + "icon_url" "https://cdn.simpleicons.org/github/181717" + }} {{- $githubMcpServers := dict "github" (dict "description" "GitHub MCP Server - access repositories, pull requests, issues, and GitHub Actions. Debug CI failures, search code, and delegate tasks to Copilot." "config" $githubConfig @@ -105,6 +115,7 @@ data: }} {{- $mcpServers = merge $mcpServers $githubMcpServers }} {{- end }} + {{- end }} {{- if .Values.mcpAddons.sentry.enabled }} {{- $sentryMcpServers := dict "sentry" (dict "description" "Sentry error tracking and monitoring - search issues, get stack traces, and analyze error patterns" diff --git a/helm/holmes/values.yaml b/helm/holmes/values.yaml index ab13932fd3..beb03478e7 100644 --- a/helm/holmes/values.yaml +++ b/helm/holmes/values.yaml @@ -325,9 +325,10 @@ mcpAddons: # Provides access to GitHub repositories, pull requests, issues, and GitHub Actions # # AUTHENTICATION (choose one): - # Option 1 - PAT: Deploys a self-hosted MCP server pod using a prebuilt image + # Option 1 - PAT: Deploys a self-hosted MCP server using the standard github-mcp image # kubectl create secret generic github-mcp-token --from-literal=token= -n - # Option 2 - GitHub App: Connects to GitHub's official remote MCP endpoint (no pod deployed) + # Option 2 - GitHub App: Deploys a self-hosted MCP server using the github-app-mcp image + # which generates and auto-refreshes installation tokens from GitHub App credentials # kubectl create secret generic github-app-secret --from-literal=GITHUB_APP_ID=... -n # # GITHUB ENTERPRISE: @@ -350,18 +351,17 @@ mcpAddons: secretKey: "token" # Key in secret (default: "token") # Option 2: GitHub App installation token - # Creates short-lived tokens from GitHub App credentials. Holmes auto-generates - # and refreshes tokens at runtime. No self-hosted MCP server pod is deployed; - # Holmes connects directly to GitHub's official remote MCP endpoint. + # Deploys a self-hosted MCP server that generates and refreshes installation tokens + # from GitHub App credentials. Uses a separate image with built-in token management. # kubectl create secret generic github-app-secret \ # --from-literal=GITHUB_APP_ID= \ # --from-literal=GITHUB_APP_INSTALLATION_ID= \ # --from-file=GITHUB_APP_PRIVATE_KEY=/path/to/private-key.pem \ # -n githubApp: - secretName: "" # Name of K8s secret containing GitHub App credentials - url: "https://api.githubcopilot.com/mcp" # MCP endpoint URL (override for custom MCP servers) - mode: "streamable-http" # MCP transport mode (override for custom MCP servers) + secretName: "" # Name of K8s secret containing GitHub App credentials + image: "github-app-mcp:1.0.0" # Image with GitHub App token management + registry: "us-central1-docker.pkg.dev/genuine-flight-317411/mcp" config: # GitHub Enterprise hostname (leave empty for github.com) From 0ee5242ed4a99246ce4fa202ef86a61e73cd70a3 Mon Sep 17 00:00:00 2001 From: "avi@robusta.dev" Date: Mon, 9 Mar 2026 09:47:13 +0200 Subject: [PATCH 3/4] remove github refresh code from holmes Signed-off-by: avi@robusta.dev --- .../builtin-toolsets/github-mcp.md | 60 ++++-- holmes/core/toolset_manager.py | 5 - holmes/utils/github_app_token_manager.py | 178 ------------------ tests/test_mcp_toolset.py | 16 +- tests/utils/test_github_app_token_manager.py | 167 ---------------- 5 files changed, 56 insertions(+), 370 deletions(-) delete mode 100644 holmes/utils/github_app_token_manager.py delete mode 100644 tests/utils/test_github_app_token_manager.py diff --git a/docs/data-sources/builtin-toolsets/github-mcp.md b/docs/data-sources/builtin-toolsets/github-mcp.md index c09e810a99..1b30e6b91a 100644 --- a/docs/data-sources/builtin-toolsets/github-mcp.md +++ b/docs/data-sources/builtin-toolsets/github-mcp.md @@ -303,27 +303,65 @@ Find the **App ID** on the App's settings page (under "About"). === "Holmes CLI" - Set the following environment variables and add the MCP server to **~/.holmes/config.yaml**: + For CLI usage, deploy the `github-app-mcp` server in your cluster and connect Holmes to it. Follow the same deployment steps as the PAT CLI setup above, but use the `github-app-mcp` image and GitHub App secret instead: - ```bash - export GITHUB_APP_ID="" - export GITHUB_APP_INSTALLATION_ID="" - export GITHUB_APP_PRIVATE_KEY="$(cat /path/to/private-key.pem)" + ```yaml + apiVersion: apps/v1 + kind: Deployment + metadata: + name: github-mcp-server + namespace: holmes-mcp + spec: + replicas: 1 + selector: + matchLabels: + app: github-mcp-server + template: + metadata: + labels: + app: github-mcp-server + spec: + containers: + - name: github-mcp + image: us-central1-docker.pkg.dev/genuine-flight-317411/mcp/github-app-mcp:1.0.0 + ports: + - containerPort: 8000 + args: + - "--stdio" + - "python3 /app/wrapper.py" + - "--port" + - "8000" + - "--outputTransport" + - "streamableHttp" + env: + - name: GITHUB_APP_ID + valueFrom: + secretKeyRef: + name: holmes-github-app + key: GITHUB_APP_ID + - name: GITHUB_APP_INSTALLATION_ID + valueFrom: + secretKeyRef: + name: holmes-github-app + key: GITHUB_APP_INSTALLATION_ID + - name: GITHUB_APP_PRIVATE_KEY + valueFrom: + secretKeyRef: + name: holmes-github-app + key: GITHUB_APP_PRIVATE_KEY ``` + Then add the MCP server to **~/.holmes/config.yaml**: + ```yaml mcp_servers: github: description: "GitHub MCP Server" config: - url: "https://api.githubcopilot.com/mcp" + url: "http://github-mcp-server.holmes-mcp.svc.cluster.local:8000/mcp" mode: "streamable-http" - extra_headers: - Authorization: "Bearer {{ env.AUTO_GENERATED_GITHUB_TOKEN }}" ``` - Holmes will automatically generate an installation token at startup and refresh it in the background. - === "Holmes Helm Chart" **Create the Kubernetes secret:** @@ -391,8 +429,6 @@ Find the **App ID** on the App's settings page (under "About"). 3. Sets the token as `GITHUB_PERSONAL_ACCESS_TOKEN` for the underlying MCP server 4. A background thread refreshes the token every 30 minutes - For CLI usage, Holmes handles token refresh in-process using `GITHUB_APP_ID`, `GITHUB_APP_INSTALLATION_ID`, and `GITHUB_APP_PRIVATE_KEY` environment variables. - ## Available Tools By default, the GitHub MCP server enables 4 toolsets that provide comprehensive access to GitHub functionality: diff --git a/holmes/core/toolset_manager.py b/holmes/core/toolset_manager.py index e685ce8981..699fe37bf9 100644 --- a/holmes/core/toolset_manager.py +++ b/holmes/core/toolset_manager.py @@ -14,7 +14,6 @@ from holmes.plugins.toolsets import load_builtin_toolsets, load_toolsets_from_config from holmes.utils.config_hash import check_and_update_config_hashes from holmes.utils.definitions import CUSTOM_TOOLSET_LOCATION -from holmes.utils.github_app_token_manager import ensure_github_app_token_env if TYPE_CHECKING: pass @@ -115,10 +114,6 @@ def _list_all_toolsets( 2. Toolsets defined in self.toolsets can override both built-in and add new custom toolsets 3. custom toolset from config can override both built-in and add new custom toolsets # for backward compatibility """ - # Ensure GitHub App token is generated and background refresh is running - # before any toolset config resolution that may reference AUTO_GENERATED_GITHUB_TOKEN - ensure_github_app_token_env() - # Load built-in toolsets # Extract search paths from custom catalog files additional_search_paths = None diff --git a/holmes/utils/github_app_token_manager.py b/holmes/utils/github_app_token_manager.py deleted file mode 100644 index dd6bc09037..0000000000 --- a/holmes/utils/github_app_token_manager.py +++ /dev/null @@ -1,178 +0,0 @@ -import logging -import os -import threading -import time -from typing import Optional - -import jwt -import requests - -logger = logging.getLogger(__name__) - -# How often to refresh the token (seconds). Default: 30 minutes. -# Configurable via GITHUB_APP_TOKEN_REFRESH_INTERVAL_SECONDS env var. -TOKEN_REFRESH_INTERVAL_SECONDS = int( - os.environ.get("GITHUB_APP_TOKEN_REFRESH_INTERVAL_SECONDS", "1800") -) - - -def _mask_token(token: str) -> str: - """Return first 4 and last 4 chars of a token for debug logging.""" - if len(token) <= 8: - return "***" - return f"{token[:4]}...{token[-4:]}" - - -class GitHubAppTokenManager: - """Manages GitHub App installation tokens with automatic refresh. - - Generates short-lived installation tokens from GitHub App credentials - (APP_ID, INSTALLATION_ID, PRIVATE_KEY). A background daemon thread - refreshes the token at a fixed interval and updates - os.environ["AUTO_GENERATED_GITHUB_TOKEN"] so that the relevant mcp servers - (e.g. extra_headers) always read a valid token. - """ - - _instance: Optional["GitHubAppTokenManager"] = None - _lock = threading.Lock() - - def __init__(self, app_id: str, installation_id: str, private_key: str): - self._app_id = app_id - self._installation_id = installation_id - self._private_key = private_key - self._refresh_thread_started = False - - @staticmethod - def has_github_app_env_vars() -> bool: - """Check if all required GitHub App environment variables are set.""" - return all( - os.environ.get(k) - for k in ("GITHUB_APP_ID", "GITHUB_APP_INSTALLATION_ID", "GITHUB_APP_PRIVATE_KEY") - ) - - @classmethod - def from_env(cls) -> Optional["GitHubAppTokenManager"]: - """Create a manager from environment variables, or return None if not configured.""" - app_id = os.environ.get("GITHUB_APP_ID") - installation_id = os.environ.get("GITHUB_APP_INSTALLATION_ID") - private_key = os.environ.get("GITHUB_APP_PRIVATE_KEY") - - if not all([app_id, installation_id, private_key]): - return None - - # CI/CD systems and secret stores may inject the private key with literal - # "\n" instead of actual newlines, which breaks JWT signing. - private_key = private_key.replace("\\n", "\n") # type: ignore[union-attr] - - return cls( - app_id=app_id, # type: ignore[arg-type] - installation_id=installation_id, # type: ignore[arg-type] - private_key=private_key, # type: ignore[arg-type] - ) - - @classmethod - def get_instance(cls) -> Optional["GitHubAppTokenManager"]: - """Get or create the singleton instance. Returns None if env vars are not set.""" - if cls._instance is not None: - return cls._instance - if not cls.has_github_app_env_vars(): - return None - with cls._lock: - if cls._instance is None: - cls._instance = cls.from_env() - return cls._instance - - def _generate_jwt(self) -> str: - """Generate a JWT signed with the GitHub App private key.""" - now = int(time.time()) - payload = { - "iat": now - 60, # Issued 60 seconds in the past for clock drift - "exp": now + 600, # Expires in 10 minutes (GitHub maximum) - "iss": self._app_id, - } - return jwt.encode(payload, self._private_key, algorithm="RS256") - - def refresh_token(self) -> str: - """Exchange a JWT for a GitHub installation access token.""" - encoded_jwt = self._generate_jwt() - - response = requests.post( - f"https://api.github.com/app/installations/{self._installation_id}/access_tokens", - headers={ - "Authorization": f"Bearer {encoded_jwt}", - "Accept": "application/vnd.github+json", - }, - timeout=30, - ) - response.raise_for_status() - - data = response.json() - token = data["token"] - - logger.info( - "GitHub App installation token refreshed (%s), expires at %s", - _mask_token(token), - data.get("expires_at", "unknown"), - ) - return token - - def start_background_refresh(self) -> None: - """Start a daemon thread that periodically refreshes the token and updates os.environ.""" - if self._refresh_thread_started: - return - self._refresh_thread_started = True - thread = threading.Thread(target=self._background_refresh_loop, daemon=True) - thread.start() - logger.info("Started GitHub App token background refresh thread") - - def _background_refresh_loop(self) -> None: - """Periodically refresh the token and update os.environ.""" - while True: - logger.debug( - "GitHub App token refresh thread sleeping for %ds", - TOKEN_REFRESH_INTERVAL_SECONDS, - ) - time.sleep(TOKEN_REFRESH_INTERVAL_SECONDS) - - try: - token = self.refresh_token() - os.environ["AUTO_GENERATED_GITHUB_TOKEN"] = token - except Exception: - logger.warning( - "Background refresh: failed to refresh GitHub App token", - exc_info=True, - ) - - -def ensure_github_app_token_env() -> None: - """If GitHub App credentials are configured, generate an installation token, - set it as AUTO_GENERATED_GITHUB_TOKEN in the environment, and start a - background thread to keep it fresh. - - This should be called early in the application lifecycle, before - environment variable substitution resolves MCP configs. - """ - # Skip if GitHub App env vars are not configured - if not GitHubAppTokenManager.has_github_app_env_vars(): - return - - # Don't override an existing token - if os.environ.get("AUTO_GENERATED_GITHUB_TOKEN"): - return - - manager = GitHubAppTokenManager.get_instance() - if manager is None: - return - - try: - token = manager.refresh_token() - os.environ["AUTO_GENERATED_GITHUB_TOKEN"] = token - logger.debug( - "Set AUTO_GENERATED_GITHUB_TOKEN (%s) from GitHub App installation token", - _mask_token(token), - ) - manager.start_background_refresh() - except Exception: - logger.warning( - "Failed to generate GitHub App installation token", exc_info=True - ) diff --git a/tests/test_mcp_toolset.py b/tests/test_mcp_toolset.py index 1694c7c464..da57215525 100644 --- a/tests/test_mcp_toolset.py +++ b/tests/test_mcp_toolset.py @@ -1799,7 +1799,7 @@ def capture_sse_client_call( class TestMCPExtraHeadersPreservedDuringEnvResolution: """Verify that load_toolsets_from_config does NOT resolve extra_headers templates. - extra_headers use Jinja2 templates like {{ env.AUTO_GENERATED_GITHUB_TOKEN }} + extra_headers use Jinja2 templates like {{ env.SOME_DYNAMIC_TOKEN }} that must be rendered at request time (so they pick up refreshed tokens). replace_env_vars_values uses the same {{ env.X }} syntax and would bake in stale values at config-load time if extra_headers were not excluded. @@ -1809,22 +1809,22 @@ class TestMCPExtraHeadersPreservedDuringEnvResolution: "os.environ", { "MY_STATIC_VAR": "resolved_value", - "AUTO_GENERATED_GITHUB_TOKEN": "ghs_initial", + "SOME_DYNAMIC_TOKEN": "initial_token", }, ) def test_extra_headers_templates_not_resolved(self): toolsets_config = { - "github": { + "my_mcp": { "type": "mcp", - "description": "GitHub MCP", + "description": "Test MCP", "config": { - "url": "https://api.githubcopilot.com/mcp", + "url": "https://example.com/mcp", "mode": "streamable-http", "headers": { "X-Static": "{{ env.MY_STATIC_VAR }}", }, "extra_headers": { - "Authorization": "Bearer {{ env.AUTO_GENERATED_GITHUB_TOKEN }}", + "Authorization": "Bearer {{ env.SOME_DYNAMIC_TOKEN }}", }, }, } @@ -1833,7 +1833,7 @@ def test_extra_headers_templates_not_resolved(self): # load_toolsets_from_config will fail to connect to the MCP server, # but we only care about the config resolution, not the connection. # Catch the validation error and inspect the config dict directly. - config = copy.deepcopy(toolsets_config["github"]) + config = copy.deepcopy(toolsets_config["my_mcp"]) # Simulate the pop/restore logic from load_toolsets_from_config saved_extra_headers = config["config"].pop("extra_headers", None) @@ -1845,7 +1845,7 @@ def test_extra_headers_templates_not_resolved(self): # extra_headers should still have the raw template (NOT resolved) assert ( config["config"]["extra_headers"]["Authorization"] - == "Bearer {{ env.AUTO_GENERATED_GITHUB_TOKEN }}" + == "Bearer {{ env.SOME_DYNAMIC_TOKEN }}" ) # regular headers SHOULD be resolved by replace_env_vars_values diff --git a/tests/utils/test_github_app_token_manager.py b/tests/utils/test_github_app_token_manager.py deleted file mode 100644 index eafdd64d71..0000000000 --- a/tests/utils/test_github_app_token_manager.py +++ /dev/null @@ -1,167 +0,0 @@ -import os -import time -from unittest.mock import MagicMock, patch - -import jwt -import pytest -from cryptography.hazmat.primitives import serialization -from cryptography.hazmat.primitives.asymmetric import rsa -from cryptography.hazmat.primitives.serialization import load_pem_private_key - -from holmes.utils.github_app_token_manager import ( - GitHubAppTokenManager, - ensure_github_app_token_env, -) - - -@pytest.fixture -def rsa_private_key(): - """Generate a real RSA private key for JWT signing.""" - key = rsa.generate_private_key(public_exponent=65537, key_size=2048) - return key.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.PKCS8, - encryption_algorithm=serialization.NoEncryption(), - ).decode() - - -@pytest.fixture -def rsa_public_key(rsa_private_key): - """Derive the public key for JWT verification.""" - key = load_pem_private_key(rsa_private_key.encode(), password=None) - return key.public_key() - - -@pytest.fixture -def token_manager(rsa_private_key): - return GitHubAppTokenManager( - app_id="12345", - installation_id="67890", - private_key=rsa_private_key, - ) - - -@pytest.fixture(autouse=True) -def reset_singleton(): - """Reset the singleton between tests.""" - GitHubAppTokenManager._instance = None - yield - GitHubAppTokenManager._instance = None - - -@pytest.fixture -def env_without_token(): - """Provide an environment with AUTO_GENERATED_GITHUB_TOKEN removed but GitHub App env vars present.""" - env = {k: v for k, v in os.environ.items() if k != "AUTO_GENERATED_GITHUB_TOKEN"} - env.update({ - "GITHUB_APP_ID": "12345", - "GITHUB_APP_INSTALLATION_ID": "67890", - "GITHUB_APP_PRIVATE_KEY": "dummy-key", - }) - with patch.dict(os.environ, env, clear=True): - yield - - -class TestGitHubAppTokenManager: - def test_generate_jwt(self, token_manager, rsa_public_key): - encoded = token_manager._generate_jwt() - decoded = jwt.decode(encoded, rsa_public_key, algorithms=["RS256"]) - - assert decoded["iss"] == "12345" - now = int(time.time()) - assert decoded["iat"] <= now - assert decoded["exp"] > now - - @patch("holmes.utils.github_app_token_manager.requests.post") - def test_refresh_token(self, mock_post, token_manager): - mock_post.return_value = MagicMock( - status_code=200, - json=lambda: {"token": "ghs_testtoken123", "expires_at": "2099-01-01T00:00:00Z"}, - ) - - token = token_manager.refresh_token() - assert token == "ghs_testtoken123" - mock_post.assert_called_once() - - @patch("holmes.utils.github_app_token_manager.requests.post") - def test_refresh_token_always_fetches(self, mock_post, token_manager): - """Each call to refresh_token should make a new API call.""" - mock_post.return_value = MagicMock( - status_code=200, - json=lambda: {"token": "ghs_fresh", "expires_at": "2099-01-01T00:00:00Z"}, - ) - - token1 = token_manager.refresh_token() - token2 = token_manager.refresh_token() - - assert token1 == token2 == "ghs_fresh" - assert mock_post.call_count == 2 - - def test_from_env_returns_none_when_not_configured(self): - with patch.dict(os.environ, {}, clear=True): - manager = GitHubAppTokenManager.from_env() - assert manager is None - - def test_from_env_returns_manager_when_configured(self, rsa_private_key): - env = { - "GITHUB_APP_ID": "111", - "GITHUB_APP_INSTALLATION_ID": "222", - "GITHUB_APP_PRIVATE_KEY": rsa_private_key, - } - with patch.dict(os.environ, env, clear=False): - manager = GitHubAppTokenManager.from_env() - assert manager is not None - assert manager._app_id == "111" - assert manager._installation_id == "222" - - def test_from_env_returns_none_with_partial_config(self): - env = {"GITHUB_APP_ID": "111"} - with patch.dict(os.environ, env, clear=True): - manager = GitHubAppTokenManager.from_env() - assert manager is None - - -class TestEnsureGitHubAppTokenEnv: - def test_does_not_override_existing_token(self): - with patch.dict( - os.environ, {"AUTO_GENERATED_GITHUB_TOKEN": "existing_token"}, clear=False - ): - ensure_github_app_token_env() - assert os.environ["AUTO_GENERATED_GITHUB_TOKEN"] == "existing_token" - - @patch("holmes.utils.github_app_token_manager.GitHubAppTokenManager.get_instance") - def test_sets_token_from_github_app(self, mock_get_instance, env_without_token): - mock_manager = MagicMock() - mock_manager.refresh_token.return_value = "ghs_generated" - mock_get_instance.return_value = mock_manager - - ensure_github_app_token_env() - assert os.environ["AUTO_GENERATED_GITHUB_TOKEN"] == "ghs_generated" - - @patch("holmes.utils.github_app_token_manager.GitHubAppTokenManager.get_instance") - def test_handles_failure_gracefully(self, mock_get_instance, env_without_token): - mock_manager = MagicMock() - mock_manager.refresh_token.side_effect = Exception("API error") - mock_get_instance.return_value = mock_manager - - ensure_github_app_token_env() - assert "AUTO_GENERATED_GITHUB_TOKEN" not in os.environ - - @patch("holmes.utils.github_app_token_manager.GitHubAppTokenManager.get_instance") - def test_noop_when_no_github_app_configured(self, mock_get_instance, env_without_token): - mock_get_instance.return_value = None - - ensure_github_app_token_env() - assert "AUTO_GENERATED_GITHUB_TOKEN" not in os.environ - - -class TestBackgroundRefresh: - @patch("holmes.utils.github_app_token_manager.GitHubAppTokenManager.get_instance") - def test_ensure_starts_background_thread(self, mock_get_instance, env_without_token): - """ensure_github_app_token_env should start the background refresh thread.""" - mock_manager = MagicMock() - mock_manager.refresh_token.return_value = "ghs_generated" - mock_get_instance.return_value = mock_manager - - ensure_github_app_token_env() - mock_manager.start_background_refresh.assert_called_once() From 5f3462043dec4f53c11a9f6ecaa71b05ef1eb2b7 Mon Sep 17 00:00:00 2001 From: "avi@robusta.dev" Date: Mon, 9 Mar 2026 10:00:52 +0200 Subject: [PATCH 4/4] docs fix Signed-off-by: avi@robusta.dev --- .../builtin-toolsets/github-mcp.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/docs/data-sources/builtin-toolsets/github-mcp.md b/docs/data-sources/builtin-toolsets/github-mcp.md index 1b30e6b91a..03577d0be0 100644 --- a/docs/data-sources/builtin-toolsets/github-mcp.md +++ b/docs/data-sources/builtin-toolsets/github-mcp.md @@ -97,7 +97,7 @@ Before deploying the GitHub MCP server, you need a GitHub Personal Access Token spec: containers: - name: github-mcp - image: us-central1-docker.pkg.dev/genuine-flight-317411/mcp/github-mcp:1.0.1 + image: me-west1-docker.pkg.dev/robusta-development/development/github-mcp:1.0.0 imagePullPolicy: IfNotPresent ports: - containerPort: 8000 @@ -303,7 +303,21 @@ Find the **App ID** on the App's settings page (under "About"). === "Holmes CLI" - For CLI usage, deploy the `github-app-mcp` server in your cluster and connect Holmes to it. Follow the same deployment steps as the PAT CLI setup above, but use the `github-app-mcp` image and GitHub App secret instead: + For CLI usage, deploy the `github-app-mcp` server in your cluster and connect Holmes to it. + + **Create the Kubernetes secret:** + + ```bash + kubectl create namespace holmes-mcp # if not already created + + kubectl create secret generic holmes-github-app \ + --from-literal=GITHUB_APP_ID= \ + --from-literal=GITHUB_APP_INSTALLATION_ID= \ + --from-file=GITHUB_APP_PRIVATE_KEY=/path/to/private-key.pem \ + -n holmes-mcp + ``` + + **Deploy the GitHub App MCP server:** ```yaml apiVersion: apps/v1