Skip to content

Latest commit

 

History

4 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

RHDH Software Template -- FastMCP Server

An RHDH (Red Hat Developer Hub) Software Template that scaffolds a FastMCP MCP server with full GitOps CI/CD on OpenShift. When a developer runs this template it creates:

  • An application repo with Python MCP server code, Dockerfile, Dev Spaces devfile, and Goose agent skills
  • A GitOps repo with Kustomize manifests managed by ArgoCD
  • A Tekton CI pipeline triggered by GitHub webhooks
  • An ArgoCD Application for continuous deployment
  • A Backstage catalog entry with CI, CD, and Kubernetes tabs

Architecture

Developer
    |
    v
 RHDH Template ──> GitHub (app repo + gitops repo + webhook)
                        |
        push to main ───┘
                        |
                        v
              Tekton EventListener
                        |
                        v
              Tekton Pipeline (fastmcp-ci)
                |           |           |              |
              clone    build+push   clone-gitops   push-gitops
                                                       |
                                                       v
                                                ArgoCD syncs
                                                       |
                                                       v
                                              MCP Server running
                                              on OpenShift

Quick Start

Before using this template, search and replace the following placeholders across the repository:

Placeholder Where Example Value
<GITHUB_ORG> template.yaml, TriggerTemplate, app-config-rhdh my-org
<CLUSTER_DOMAIN> template.yaml, skeleton/catalog-info.yaml apps.ocp.example.com
<ARGOCD_SERVER_URL> dynamic-plugins-rhdh https://openshift-gitops-server-openshift-gitops.apps.ocp.example.com

Then follow the installation guide below to set up the OpenShift cluster infrastructure and RHDH configuration.


Prerequisites

The following operators must be installed on the OpenShift cluster:

Operator Purpose
Red Hat Developer Hub (RHDH) Internal developer portal
OpenShift Pipelines (Tekton) CI pipeline execution
OpenShift GitOps (ArgoCD) Continuous deployment
OpenShift Dev Spaces Cloud-based development workspaces

You also need a GitHub account with a personal access token that has repo, admin:repo_hook, and workflow scopes.


Installation Guide

1. Create the mcp-servers namespace

oc new-project mcp-servers

2. Create the RBAC for the pipeline service account

The pipeline ServiceAccount in mcp-servers needs edit and system:image-builder roles to build and deploy applications.

oc adm policy add-role-to-user edit system:serviceaccount:mcp-servers:pipeline -n mcp-servers
oc adm policy add-role-to-user system:image-builder system:serviceaccount:mcp-servers:pipeline -n mcp-servers

3. Create the shared Tekton Pipeline

This pipeline is shared by all MCP servers. It clones the source, builds the container image, then updates the GitOps repo with the new image tag.

apiVersion: tekton.dev/v1
kind: Pipeline
metadata:
  name: fastmcp-ci
  namespace: mcp-servers
spec:
  params:
    - name: git-url
      type: string
    - name: git-revision
      type: string
      default: main
    - name: repo-name
      type: string
    - name: deploy-namespace
      type: string
      default: mcp-servers
    - name: gitops-repo-url
      type: string
      description: HTTPS URL of the GitOps repository
  tasks:
    - name: clone
      params:
        - name: URL
          value: $(params.git-url)
        - name: REVISION
          value: $(params.git-revision)
      taskRef:
        resolver: cluster
        params:
          - name: kind
            value: task
          - name: name
            value: git-clone
          - name: namespace
            value: openshift-pipelines
      workspaces:
        - name: output
          workspace: source
    - name: build-push
      runAfter: [clone]
      params:
        - name: IMAGE
          value: >-
            image-registry.openshift-image-registry.svc:5000/$(params.deploy-namespace)/$(params.repo-name):$(tasks.clone.results.COMMIT)
        - name: TLS_VERIFY
          value: "false"
      taskRef:
        resolver: cluster
        params:
          - name: kind
            value: task
          - name: name
            value: buildah
          - name: namespace
            value: openshift-pipelines
      workspaces:
        - name: source
          workspace: source
    - name: clone-gitops
      runAfter: [build-push]
      params:
        - name: URL
          value: $(params.gitops-repo-url)
        - name: REVISION
          value: main
        - name: DEPTH
          value: "0"
      taskRef:
        resolver: cluster
        params:
          - name: kind
            value: task
          - name: name
            value: git-clone
          - name: namespace
            value: openshift-pipelines
      workspaces:
        - name: output
          workspace: gitops
        - name: basic-auth
          workspace: git-credentials
    - name: push-gitops
      runAfter: [clone-gitops]
      params:
        - name: GIT_USER_NAME
          value: Tekton CI
        - name: GIT_USER_EMAIL
          value: tekton-ci@openshift.local
        - name: DELETE_EXISTING
          value: "false"
        - name: GIT_SCRIPT
          value: |
            COMMIT_SHA="$(tasks.clone.results.COMMIT)"
            IMAGE="image-registry.openshift-image-registry.svc:5000/$(params.deploy-namespace)/$(params.repo-name):${COMMIT_SHA}"
            git checkout -b main 2>/dev/null || git checkout main
            cd manifests/base
            sed -i "s|image:.*|image: ${IMAGE}|" deployment.yaml
            cd /workspace/source
            git add -A
            git diff --cached --quiet && echo "No changes to commit" && exit 0
            git commit -m "ci: update image to ${COMMIT_SHA:0:8}"
            git push origin HEAD:main
      taskRef:
        resolver: cluster
        params:
          - name: kind
            value: task
          - name: name
            value: git-cli
          - name: namespace
            value: openshift-pipelines
      workspaces:
        - name: source
          workspace: gitops
        - name: basic-auth
          workspace: git-credentials
  workspaces:
    - name: source
    - name: gitops
    - name: git-credentials

4. Create the GitHub webhook secret

This secret is used by the EventListener to validate incoming GitHub webhook payloads. The value must match the webhookSecret in template.yaml.

oc create secret generic github-webhook-secret \
  --from-literal=webhook-secret=pac-webhook-shared-secret \
  -n mcp-servers

5. Create the GitHub basic-auth secret for GitOps pushes

The pipeline needs credentials to push image tag updates to the GitOps repo.

oc create secret generic github-basic-auth \
  --type=kubernetes.io/basic-auth \
  --from-literal=username=<GITHUB_USERNAME> \
  --from-literal=password=<GITHUB_TOKEN> \
  -n mcp-servers

oc annotate secret github-basic-auth \
  "tekton.dev/git-0=https://github.com" \
  -n mcp-servers

6. Create the Tekton Triggers (EventListener, TriggerBinding, TriggerTemplate)

TriggerBinding -- extracts values from the GitHub webhook payload:

apiVersion: triggers.tekton.dev/v1beta1
kind: TriggerBinding
metadata:
  name: github-push-binding
  namespace: mcp-servers
spec:
  params:
    - name: git-url
      value: $(body.repository.clone_url)
    - name: git-revision
      value: $(body.after)
    - name: repo-name
      value: $(body.repository.name)

TriggerTemplate -- creates a PipelineRun with Backstage labels for CI tab visibility:

apiVersion: triggers.tekton.dev/v1beta1
kind: TriggerTemplate
metadata:
  name: fastmcp-deploy-template
  namespace: mcp-servers
spec:
  params:
    - name: git-url
    - name: git-revision
    - name: repo-name
  resourcetemplates:
    - apiVersion: tekton.dev/v1
      kind: PipelineRun
      metadata:
        generateName: $(tt.params.repo-name)-run-
        namespace: mcp-servers
        labels:
          app: $(tt.params.repo-name)
          backstage.io/kubernetes-id: $(tt.params.repo-name)
          tekton.dev/pipeline: fastmcp-ci
      spec:
        pipelineRef:
          name: fastmcp-ci
        params:
          - name: git-url
            value: $(tt.params.git-url)
          - name: git-revision
            value: $(tt.params.git-revision)
          - name: repo-name
            value: $(tt.params.repo-name)
          - name: deploy-namespace
            value: mcp-servers
          - name: gitops-repo-url
            value: https://github.com/<GITHUB_ORG>/$(tt.params.repo-name)-gitops.git
        workspaces:
          - name: source
            volumeClaimTemplate:
              spec:
                accessModes: [ReadWriteOnce]
                resources:
                  requests:
                    storage: 1Gi
          - name: gitops
            volumeClaimTemplate:
              spec:
                accessModes: [ReadWriteOnce]
                resources:
                  requests:
                    storage: 256Mi
          - name: git-credentials
            secret:
              secretName: github-basic-auth

Replace <GITHUB_ORG> with the GitHub organization or username that owns the repos (must match allowedOwners in template.yaml).

EventListener -- receives GitHub webhooks and filters for pushes to main:

apiVersion: triggers.tekton.dev/v1beta1
kind: EventListener
metadata:
  name: fastmcp-listener
  namespace: mcp-servers
spec:
  serviceAccountName: pipeline
  triggers:
    - name: github-push
      bindings:
        - ref: github-push-binding
      interceptors:
        - ref:
            name: github
            kind: ClusterInterceptor
          params:
            - name: secretRef
              value:
                secretName: github-webhook-secret
                secretKey: webhook-secret
            - name: eventTypes
              value: [push]
        - ref:
            name: cel
            kind: ClusterInterceptor
          params:
            - name: filter
              value: "body.ref == 'refs/heads/main'"
      template:
        ref: fastmcp-deploy-template

7. Expose the EventListener

Create a route so GitHub can reach the EventListener. Note the URL -- it will be used in template.yaml for the webhookUrl.

oc expose svc el-fastmcp-listener -n mcp-servers

WEBHOOK_URL=$(oc get route el-fastmcp-listener -n mcp-servers -o jsonpath='{.spec.host}')
echo "Webhook URL: https://$WEBHOOK_URL"

If TLS is required, create an edge-terminated route instead:

oc create route edge el-fastmcp-listener \
  --service=el-fastmcp-listener \
  --insecure-policy=Redirect \
  -n mcp-servers

Update template.yaml line 142 (webhookUrl) with this route URL.


RHDH Configuration

All configuration lives in three resources in the RHDH namespace (typically rhdh-operator).

1. Secret: rhdh-secrets

Create a secret with all credentials the plugins need:

oc create secret generic rhdh-secrets \
  --from-literal=GITHUB_TOKEN=<github-pat> \
  --from-literal=K8S_SA_TOKEN=<sa-token> \
  --from-literal=ARGOCD_USERNAME=admin \
  --from-literal=ARGOCD_PASSWORD=<argocd-admin-password> \
  --from-literal=ARGOCD_AUTH_TOKEN=<argocd-auth-token> \
  --from-literal=KEYCLOAK_BASE_URL=<keycloak-oidc-metadata-url> \
  --from-literal=KEYCLOAK_CLIENT_ID=<client-id> \
  --from-literal=KEYCLOAK_CLIENT_SECRET=<client-secret> \
  --from-literal=KEYCLOAK_REALM=<realm> \
  --from-literal=KEYCLOAK_LOGIN_REALM=<login-realm> \
  -n rhdh-operator

How to get K8S_SA_TOKEN:

# Create a ServiceAccount for the K8s plugin
oc create sa backstage-k8s-plugin -n rhdh-operator

# Create the ClusterRole (see section below)
# Bind it
oc create clusterrolebinding backstage-k8s-reader-binding \
  --clusterrole=backstage-k8s-reader \
  --serviceaccount=rhdh-operator:backstage-k8s-plugin

# Create a long-lived token
oc create token backstage-k8s-plugin -n rhdh-operator --duration=8760h

2. ClusterRole: backstage-k8s-reader

This role gives the Kubernetes plugin read access to workloads, Tekton resources, ArgoCD applications, routes, and Dev Spaces clusters.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: backstage-k8s-reader
rules:
  - apiGroups: [""]
    resources: [pods, pods/log, services, configmaps, events,
                limitranges, resourcequotas]
    verbs: [get, list, watch]
  - apiGroups: [apps]
    resources: [deployments, replicasets, statefulsets, daemonsets]
    verbs: [get, list, watch]
  - apiGroups: [batch]
    resources: [jobs, cronjobs]
    verbs: [get, list, watch]
  - apiGroups: [autoscaling]
    resources: [horizontalpodautoscalers]
    verbs: [get, list, watch]
  - apiGroups: [networking.k8s.io]
    resources: [ingresses]
    verbs: [get, list, watch]
  - apiGroups: [metrics.k8s.io]
    resources: [pods]
    verbs: [get, list]
  - apiGroups: [tekton.dev]
    resources: [pipelines, pipelineruns, taskruns, tasks]
    verbs: [get, list, watch]
  - apiGroups: [triggers.tekton.dev]
    resources: [eventlisteners, triggerbindings, triggertemplates]
    verbs: [get, list, watch]
  - apiGroups: [route.openshift.io]
    resources: [routes]
    verbs: [get, list, watch]
  - apiGroups: [org.eclipse.che]
    resources: [checlusters]
    verbs: [get, list]
  - apiGroups: [argoproj.io]
    resources: [applications, appprojects]
    verbs: [get, list, watch]

3. ConfigMap: app-config-rhdh

Core RHDH application config. Replace placeholder values with your environment-specific URLs and credentials.

apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config-rhdh
  namespace: rhdh-operator
data:
  app-config.yaml: |
    app:
      title: Red Hat Developer Hub
    integrations:
      github:
        - host: github.com
          token: ${GITHUB_TOKEN}
    signInPage: oidc
    auth:
      session:
        secret: <random-session-secret>
      environment: production
      providers:
        oidc:
          production:
            metadataUrl: ${KEYCLOAK_BASE_URL}
            clientId: ${KEYCLOAK_CLIENT_ID}
            clientSecret: ${KEYCLOAK_CLIENT_SECRET}
            prompt: auto
            signIn:
              resolvers:
                - resolver: preferredUsernameMatchingUserEntityName
                - resolver: emailLocalPartMatchingUserEntityName
                  dangerouslyAllowSignInWithoutUserInCatalog: true
    catalog:
      locations:
        - type: url
          target: https://github.com/<GITHUB_ORG>/rhdh-mcp-template/blob/main/location.yaml
          rules:
            - allow: [Template]
      providers:
        keycloakOrg:
          default:
            baseUrl: <KEYCLOAK_BASE_URL>
            clientId: ${KEYCLOAK_CLIENT_ID}
            clientSecret: ${KEYCLOAK_CLIENT_SECRET}
            realm: ${KEYCLOAK_REALM}
            loginRealm: ${KEYCLOAK_LOGIN_REALM}
            schedule:
              frequency: { minutes: 1 }
              timeout: { minutes: 1 }
              initialDelay: { seconds: 15 }

4. ConfigMap: dynamic-plugins-rhdh

This is where every plugin is enabled. Each entry below is required for the template to work end-to-end with CI/CD visibility.

apiVersion: v1
kind: ConfigMap
metadata:
  name: dynamic-plugins-rhdh
  namespace: rhdh-operator
data:
  dynamic-plugins.yaml: |
    includes:
      - dynamic-plugins.default.yaml
    plugins:
      # --- GitHub scaffolder (publish:github, github:webhook) ---
      - package: ./dynamic-plugins/dist/backstage-plugin-scaffolder-backend-module-github-dynamic
        disabled: false

      # --- Kubernetes backend (cluster API access) ---
      - package: ./dynamic-plugins/dist/backstage-plugin-kubernetes-backend-dynamic
        disabled: false
        pluginConfig:
          kubernetes:
            serviceLocatorMethod:
              type: multiTenant
            clusterLocatorMethods:
              - type: config
                clusters:
                  - name: local-cluster
                    url: https://kubernetes.default.svc
                    authProvider: serviceAccount
                    skipTLSVerify: true
                    serviceAccountToken: ${K8S_SA_TOKEN}
            customResources:
              - group: 'tekton.dev'
                apiVersion: 'v1'
                plural: 'pipelineruns'
              - group: 'tekton.dev'
                apiVersion: 'v1'
                plural: 'taskruns'

      # --- Kubernetes frontend (Kubernetes tab) ---
      - package: ./dynamic-plugins/dist/backstage-plugin-kubernetes
        disabled: false
        pluginConfig:
          dynamicPlugins:
            frontend:
              backstage.plugin-kubernetes:
                mountPoints:
                  - mountPoint: entity.page.kubernetes/cards
                    importName: EntityKubernetesContent
                    config:
                      layout:
                        gridColumn: 1 / -1
                    if:
                      anyOf:
                        - hasAnnotation: backstage.io/kubernetes-id
                        - hasAnnotation: backstage.io/kubernetes-namespace

      # --- Tekton CI tab ---
      - package: ./dynamic-plugins/dist/backstage-community-plugin-tekton
        disabled: false
        pluginConfig:
          dynamicPlugins:
            frontend:
              backstage-community.plugin-tekton:
                mountPoints:
                  - mountPoint: entity.page.ci/cards
                    importName: TektonCI
                    config:
                      layout:
                        gridColumn: 1 / -1
                    if:
                      allOf:
                        - isTektonCIAvailable

      # --- ArgoCD scaffolder (argocd:create-resources action) ---
      - package: ./dynamic-plugins/dist/roadiehq-scaffolder-backend-argocd-dynamic
        disabled: false
        pluginConfig:
          argocd:
            username: ${ARGOCD_USERNAME}
            password: ${ARGOCD_PASSWORD}
            appLocatorMethods:
              - type: config
                instances:
                  - name: openshift-gitops
                    url: <ARGOCD_SERVER_URL>
                    token: ${ARGOCD_AUTH_TOKEN}

      # --- ArgoCD backend (CD tab data) ---
      - package: ./dynamic-plugins/dist/roadiehq-backstage-plugin-argo-cd-backend-dynamic
        disabled: false
        pluginConfig:
          argocd:
            username: ${ARGOCD_USERNAME}
            password: ${ARGOCD_PASSWORD}
            appLocatorMethods:
              - type: config
                instances:
                  - name: openshift-gitops
                    url: <ARGOCD_SERVER_URL>
                    token: ${ARGOCD_AUTH_TOKEN}

      # --- ArgoCD frontend (CD tab UI) ---
      - package: oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/backstage-community-plugin-argocd:bs_1.45.3__2.4.3
        disabled: false
        pluginConfig:
          dynamicPlugins:
            frontend:
              backstage-community.plugin-argocd:
                translationResources:
                  - importName: argocdTranslations
                    module: Alpha
                    ref: argocdTranslationRef
                mountPoints:
                  - mountPoint: entity.page.overview/cards
                    importName: ArgocdDeploymentSummary
                    config:
                      layout:
                        gridColumnEnd:
                          lg: span 8
                          xs: span 12
                    if:
                      allOf:
                        - isArgocdConfigured
                  - mountPoint: entity.page.cd/cards
                    importName: ArgocdDeploymentLifecycle
                    config:
                      layout:
                        gridColumn: 1 / -1
                    if:
                      allOf:
                        - isArgocdConfigured

Replace <ARGOCD_SERVER_URL> with your ArgoCD route, e.g. https://openshift-gitops-server-openshift-gitops.apps.<cluster-domain>

The ArgoCD instance name openshift-gitops must match the argoInstance value in template.yaml step createArgoApp.

5. Backstage CR

The Backstage custom resource must reference the ConfigMaps and Secrets above, and use a ServiceAccount that has the mounted token:

apiVersion: rhdh.redhat.com/v1alpha5
kind: Backstage
metadata:
  name: developer-hub
  namespace: rhdh-operator
spec:
  application:
    appConfig:
      configMaps:
        - name: app-config-rhdh
    dynamicPluginsConfigMapName: dynamic-plugins-rhdh
    extraEnvs:
      secrets:
        - name: rhdh-secrets
    route:
      enabled: true
  database:
    enableLocalDb: true

Customization

Changing the GitHub organization

Update these locations:

File Field
template.yaml line 82 allowedOwners
template.yaml line 142 webhookUrl (cluster-specific)
TriggerTemplate gitops-repo-url value

Changing the target namespace

The default namespace is mcp-servers. To change it, update:

  • template.yaml line 73 (default: mcp-servers)
  • All Tekton resources (Pipeline, TriggerTemplate, EventListener)
  • ArgoCD Application target namespace

Adding more Tekton custom resources

If you need to see additional Tekton resources in the Kubernetes tab, add them to the customResources list in the Kubernetes backend plugin config.


Troubleshooting

CI tab is blank

The most common cause is missing customResources in the Kubernetes backend plugin config. The backend only fetches standard Kubernetes resources by default. You must explicitly list pipelineruns and taskruns under customResources.

Also verify:

  • PipelineRuns have label backstage.io/kubernetes-id: <component-name>
  • Component has annotation janus-idp.io/tekton: <component-name>
  • Component has annotation backstage.io/kubernetes-namespace: mcp-servers

CD tab is blank

Verify:

  • Component has annotation argocd/app-name: <component-name>
  • ArgoCD plugin is configured with the correct instance URL and token
  • The ArgoCD Application exists in the openshift-gitops namespace

Template fails at "Create ArgoCD Application"

The argocd:create-resources scaffolder action requires the roadiehq-scaffolder-backend-argocd-dynamic plugin to be enabled. Check the scaffolder log for available actions:

oc logs deploy/backstage-developer-hub -n rhdh-operator -c backstage-backend \
  | grep "actions enabled"

Template fails at "Create GitHub Repository"

Ensure backstage-plugin-scaffolder-backend-module-github-dynamic is set to disabled: false in the dynamic plugins ConfigMap.

About

RHDH Software Template for scaffolding FastMCP MCP servers with Tekton CI and ArgoCD CD on OpenShift

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages