Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
.git
.github
__pycache__
*.pyc
*.pyo
*.pyd
.Python
*.so
*.egg
*.egg-info
dist
build
.pytest_cache
.ruff_cache
.venv
venv
test
tests
*.md
!README.md
examples
scripts
common-dev-assets
.secrets.baseline
.pre-commit-config.yaml
renovate.json
terraform
deploy
docs
67 changes: 67 additions & 0 deletions .github/workflows/build-image.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: Build Container Image

on:
push:
tags:
- "v*"
pull_request:
branches:
- main

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}

jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to GHCR
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,prefix=sha-
type=raw,value=latest,enable={{is_default_branch}}

- name: Determine version
id: version
run: |
if [[ "${{ github.ref_type }}" == "tag" ]]; then
echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
else
echo "version=0.0.0-dev+${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
fi

- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
VERSION=${{ steps.version.outputs.version }}
cache-from: type=gha
cache-to: type=gha,mode=max
35 changes: 35 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
FROM registry.access.redhat.com/ubi9/python-312:latest AS builder
USER root
WORKDIR /app
COPY --from=ghcr.io/astral-sh/uv:0.5.11 /uv /usr/local/bin/uv
COPY pyproject.toml README.md ./
COPY tim_mcp ./tim_mcp
COPY static ./static
RUN chown -R 1001:0 /app && chmod -R g+w /app

# hatch-vcs needs a version since .git is absent
ARG VERSION=0.0.0
ENV SETUPTOOLS_SCM_PRETEND_VERSION=${VERSION}

USER 1001
RUN UV_CACHE_DIR=/app/.uv-cache uv sync --no-dev && rm -rf /app/.uv-cache

FROM registry.access.redhat.com/ubi9/python-312:latest
USER root
WORKDIR /app
COPY --from=builder /app/.venv /app/.venv
COPY tim_mcp ./tim_mcp
COPY static ./static
COPY pyproject.toml ./
RUN chown -R 1001:0 /app && chmod -R g+w /app
USER 1001

ENV PATH="/app/.venv/bin:$PATH" \
PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PORT=8080
EXPOSE 8080

HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8080/health').read()"
CMD ["python", "-m", "tim_mcp.main", "--http", "--host", "0.0.0.0"]
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,47 @@ Once configured, your AI assistant can help you build IBM Cloud infrastructure f
- **GitHub API Rate Limit Docs:** https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api
- **Terraform Registry (IBM Modules):** https://registry.terraform.io/namespaces/terraform-ibm-modules

## Container and OpenShift Deployment

TIM-MCP can be deployed as a container on OpenShift or any Kubernetes cluster.

### Quick Start (Docker)

```bash
# Build the container image
docker build --build-arg VERSION=0.1.0 -t tim-mcp:latest .

# Run locally
docker run -p 8080:8080 -e GITHUB_TOKEN=<your_token> tim-mcp:latest

# Test
curl http://localhost:8080/health
```

### OpenShift Deployment

Deploy to OpenShift using the provided Kustomize manifests:

```bash
# Create namespace
oc new-project tim-mcp

# Deploy (standalone — direct access via OpenShift Route)
oc apply -k deploy/openshift/overlays/standalone

# Or deploy behind the Red Hat MCP Gateway
oc apply -k deploy/openshift/overlays/mcp-gateway

# Set the GitHub token secret
oc create secret generic tim-mcp-secrets \
--from-literal=github-token=<YOUR_TOKEN> \
-n tim-mcp --dry-run=client -o yaml | oc apply -f -
```

For full details on the OpenShift and MCP Gateway deployment architecture, see [docs/openshift-mcp-gateway.md](docs/openshift-mcp-gateway.md).

For HTTP deployment options (nginx, systemd, Docker Compose, Kubernetes), see [examples/http_deployment.md](examples/http_deployment.md).

## For Developers

If you're interested in contributing to TIM-MCP or modifying the server itself, please see the [Development Guide](DEVELOPMENT.md) for detailed instructions on development setup, transport modes, and advanced configuration options.
Expand Down
67 changes: 67 additions & 0 deletions deploy/openshift/base/deployment.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: tim-mcp
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: tim-mcp
template:
spec:
containers:
- name: tim-mcp
image: ghcr.io/terraform-ibm-modules/tim-mcp:latest
ports:
- containerPort: 8080
protocol: TCP
name: http
envFrom:
- configMapRef:
name: tim-mcp-config
env:
- name: GITHUB_TOKEN
valueFrom:
secretKeyRef:
name: tim-mcp-secrets
key: github-token
- name: TIM_PER_IP_RATE_LIMIT
value: ""
livenessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 10
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
securityContext:
runAsNonRoot: true
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
seccompProfile:
type: RuntimeDefault
capabilities:
drop:
- ALL
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
27 changes: 27 additions & 0 deletions deploy/openshift/base/kustomization.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

namespace: tim-mcp

commonLabels:
app.kubernetes.io/name: tim-mcp
app.kubernetes.io/component: mcp-server

resources:
- deployment.yaml
- service.yaml

configMapGenerator:
- name: tim-mcp-config
literals:
- TIM_LOG_LEVEL=INFO
- TIM_STRUCTURED_LOGGING=true
- TIM_ALLOWED_NAMESPACES=terraform-ibm-modules

secretGenerator:
- name: tim-mcp-secrets
literals:
- github-token=REPLACE_ME

generatorOptions:
disableNameSuffixHash: true
11 changes: 11 additions & 0 deletions deploy/openshift/base/service.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
apiVersion: v1
kind: Service
metadata:
name: tim-mcp
spec:
type: ClusterIP
ports:
- port: 8080
targetPort: http
protocol: TCP
name: http
16 changes: 16 additions & 0 deletions deploy/openshift/overlays/mcp-gateway/httproute.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: tim-mcp
spec:
parentRefs:
- name: mcp-gateway
namespace: mcp-gateway
rules:
- matches:
- path:
type: PathPrefix
value: /tim
backendRefs:
- name: tim-mcp
port: 8080
8 changes: 8 additions & 0 deletions deploy/openshift/overlays/mcp-gateway/kustomization.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
- ../../base
- httproute.yaml
- mcpserverregistration.yaml
- networkpolicy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
apiVersion: gateway.kuadrant.io/v1alpha1
kind: MCPServerRegistration
metadata:
name: tim-mcp
spec:
toolPrefix: tim_
mcpServerRef:
httpRouteRef:
name: tim-mcp
path: /mcp
18 changes: 18 additions & 0 deletions deploy/openshift/overlays/mcp-gateway/networkpolicy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: tim-mcp-allow-gateway
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: tim-mcp
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: mcp-gateway
ports:
- port: 8080
protocol: TCP
6 changes: 6 additions & 0 deletions deploy/openshift/overlays/standalone/kustomization.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
- ../../base
- route.yaml
14 changes: 14 additions & 0 deletions deploy/openshift/overlays/standalone/route.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
apiVersion: route.openshift.io/v1
kind: Route
metadata:
name: tim-mcp
spec:
to:
kind: Service
name: tim-mcp
weight: 100
port:
targetPort: http
tls:
termination: edge
insecureEdgeTerminationPolicy: Redirect
Loading