Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .claude/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# GPS - Global Positioning System
# GPS - Global Positioning System MCP Server

Read-only caching tier for org and engineering data. Materializes people, teams, issues, features, releases, and governance into a SQLite database. Optimized for sub-millisecond agent queries over MCP — no auth, no rate limits.

Expand Down
12 changes: 6 additions & 6 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,8 @@ jobs:
--docker-server=quay.io \
--docker-username="${{ secrets.QUAY_USERNAME }}" \
--docker-password="${{ secrets.QUAY_PASSWORD }}" \
--namespace=gps --dry-run=client -o yaml | oc apply -f -
oc secrets link default quay-pull-secret --for=pull -n gps 2>/dev/null || true
--namespace=gps-mcp-server --dry-run=client -o yaml | oc apply -f -
oc secrets link default quay-pull-secret --for=pull -n gps-mcp-server 2>/dev/null || true

- name: Deploy to OpenShift
env:
Expand All @@ -178,10 +178,10 @@ jobs:
cd deploy/k8s/overlays/openshift
kustomize edit set image "python:3.11-slim=quay.io/ambient_code/gps:${TAG}"
kustomize build . > /dev/null
oc apply -k . -n gps
oc rollout status deployment/mcp-server -n gps --timeout=120s
oc apply -k . -n gps-mcp-server
oc rollout status deployment/gps-mcp-server -n gps-mcp-server --timeout=120s

- name: Show deployment status
run: |
oc get pods -n gps
oc get routes -n gps
oc get pods -n gps-mcp-server
oc get routes -n gps-mcp-server
5 changes: 5 additions & 0 deletions Containerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ WORKDIR /app
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
COPY mcp_server.py VERSION ./
COPY data/gps.db data/DATA_CATALOG.yaml data/

# OpenShift runs containers as arbitrary UID — ensure writable dirs
ENV UV_CACHE_DIR=/tmp/uv-cache
RUN chmod -R g+rwX /app

EXPOSE 8000
HEALTHCHECK CMD curl -f http://localhost:8000/health || exit 1
CMD ["uv", "run", "--script", "mcp_server.py", "--http", "--port", "8000"]
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# GPS - Global Positioning System
# GPS - Global Positioning System MCP Server

A read-only caching tier that gives agents and humans sub-millisecond access to org and engineering data. GPS materializes people, teams, issues, features, release schedules, component mappings, and governance documents into a SQLite database and serves them over [MCP](https://modelcontextprotocol.io) — optimized for high-frequency agent queries with zero upstream latency.

Expand Down
70 changes: 63 additions & 7 deletions deploy/deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
NAMESPACE="${NAMESPACE:-gps}"
NAMESPACE="${NAMESPACE:-gps-mcp-server}"
OVERLAY="${OVERLAY:-base}"

# Detect kubectl or oc
Expand All @@ -18,14 +18,17 @@ usage() {
echo ""
echo "Commands:"
echo " build Build gps.db and run tests"
echo " image Build and push container image to registry"
echo " apply Apply Kubernetes manifests"
echo " status Show deployment status"
echo " logs Tail MCP server logs"
echo " all build + apply"
echo " all image + build + apply"
echo ""
echo "Environment:"
echo " NAMESPACE Kubernetes namespace (default: gps)"
echo " OVERLAY Kustomize overlay (default: base, option: openshift)"
echo " NAMESPACE Kubernetes namespace (default: gps-mcp-server)"
echo " OVERLAY Kustomize overlay (default: base, option: openshift)"
echo " IMAGE_REGISTRY Override container registry host"
echo " IMAGE Override full image reference for apply"
}

# Parse --overlay flag from any position
Expand Down Expand Up @@ -54,18 +57,70 @@ else
KUSTOMIZE_DIR="$SCRIPT_DIR/k8s/base"
fi

cmd_image() {
echo "=== Building and pushing container image ==="

if [[ -n "${IMAGE_REGISTRY:-}" ]]; then
REGISTRY="$IMAGE_REGISTRY"
else
REGISTRY=$($KUBECTL get route default-route -n openshift-image-registry -o jsonpath='{.spec.host}')
fi

echo "Registry: $REGISTRY"
echo "Namespace: $NAMESPACE"

echo "=== Logging into registry ==="
docker login "$REGISTRY" -u "$($KUBECTL whoami)" -p "$($KUBECTL whoami -t)"

local IMAGE_REF="$REGISTRY/$NAMESPACE/gps-mcp-server:latest"
echo "=== Building $IMAGE_REF ==="
docker build --provenance=false --platform=linux/amd64 -f "$REPO_ROOT/Containerfile" -t "$IMAGE_REF" "$REPO_ROOT"

echo "=== Pushing $IMAGE_REF ==="
docker push "$IMAGE_REF"

echo "=== Image pushed successfully ==="
}

cmd_build() {
echo "=== Building GPS database ==="
uv run "$REPO_ROOT/scripts/build_db.py" --force
"$REPO_ROOT/scripts/test.sh"
}

cmd_apply() {
echo "=== Ensuring namespace $NAMESPACE exists ==="
if ! $KUBECTL get namespace "$NAMESPACE" &>/dev/null; then
if [[ "$KUBECTL" == "oc" ]]; then
oc new-project "$NAMESPACE" --display-name="GPS"
else
kubectl create namespace "$NAMESPACE"
fi
fi

echo "=== Applying manifests to $NAMESPACE (overlay: $OVERLAY) ==="

# Set image override if specified or defaulting for OpenShift
local EFFECTIVE_IMAGE="${IMAGE:-}"
if [[ -z "$EFFECTIVE_IMAGE" && "$OVERLAY" == "openshift" ]]; then
EFFECTIVE_IMAGE="image-registry.openshift-image-registry.svc:5000/$NAMESPACE/gps-mcp-server:latest"
fi

if [[ -n "$EFFECTIVE_IMAGE" ]]; then
echo "=== Setting image to $EFFECTIVE_IMAGE ==="
cp "$KUSTOMIZE_DIR/kustomization.yaml" "$KUSTOMIZE_DIR/kustomization.yaml.bak"
(cd "$KUSTOMIZE_DIR" && kustomize edit set image "python:3.11-slim=$EFFECTIVE_IMAGE")
fi

$KUBECTL apply -k "$KUSTOMIZE_DIR"

# Restore original kustomization.yaml
if [[ -n "$EFFECTIVE_IMAGE" ]]; then
mv "$KUSTOMIZE_DIR/kustomization.yaml.bak" "$KUSTOMIZE_DIR/kustomization.yaml"
fi
echo ""
echo "=== Waiting for rollout ==="
$KUBECTL -n "$NAMESPACE" rollout status deployment/mcp-server --timeout=120s
$KUBECTL -n "$NAMESPACE" rollout status deployment/gps-mcp-server --timeout=120s
echo ""
cmd_status
}
Expand All @@ -84,14 +139,15 @@ cmd_status() {
}

cmd_logs() {
$KUBECTL -n "$NAMESPACE" logs -f deployment/mcp-server
$KUBECTL -n "$NAMESPACE" logs -f deployment/gps-mcp-server
}

case "${1:-}" in
build) cmd_build ;;
image) cmd_image ;;
apply) cmd_apply ;;
status) cmd_status ;;
logs) cmd_logs ;;
all) cmd_build && cmd_apply ;;
all) cmd_image && cmd_build && cmd_apply ;;
*) usage; exit 1 ;;
esac
12 changes: 6 additions & 6 deletions deploy/k8s/base/deployment.yaml
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: mcp-server
namespace: gps
name: gps-mcp-server
namespace: gps-mcp-server
labels:
app: mcp-server
app: gps-mcp-server
app.kubernetes.io/part-of: gps
spec:
replicas: 1
selector:
matchLabels:
app: mcp-server
app: gps-mcp-server
template:
metadata:
labels:
app: mcp-server
app: gps-mcp-server
spec:
imagePullSecrets:
- name: quay-pull-secret
containers:
- name: mcp-server
- name: gps-mcp-server
image: python:3.11-slim
command: ["uv", "run", "mcp_server.py", "--http", "--port", "8000"]
ports:
Expand Down
1 change: 0 additions & 1 deletion deploy/k8s/base/kustomization.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- namespace.yaml
- pvcs.yaml
- deployment.yaml
- service.yaml
2 changes: 1 addition & 1 deletion deploy/k8s/base/namespace.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
apiVersion: v1
kind: Namespace
metadata:
name: gps
name: gps-mcp-server
labels:
app.kubernetes.io/part-of: gps
2 changes: 1 addition & 1 deletion deploy/k8s/base/pvcs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: gps-data
namespace: gps
namespace: gps-mcp-server
spec:
accessModes: [ReadWriteOnce]
resources:
Expand Down
6 changes: 3 additions & 3 deletions deploy/k8s/base/service.yaml
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
apiVersion: v1
kind: Service
metadata:
name: mcp-server
namespace: gps
name: gps-mcp-server
namespace: gps-mcp-server
spec:
selector:
app: mcp-server
app: gps-mcp-server
ports:
- port: 8000
targetPort: 8000
Expand Down
5 changes: 5 additions & 0 deletions deploy/k8s/overlays/openshift/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,8 @@ kind: Kustomization
resources:
- ../../base
- route.yaml
patches:
- target:
kind: Deployment
name: gps-mcp-server
path: remove-pull-secret.yaml
2 changes: 2 additions & 0 deletions deploy/k8s/overlays/openshift/remove-pull-secret.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- op: remove
path: /spec/template/spec/imagePullSecrets
6 changes: 3 additions & 3 deletions deploy/k8s/overlays/openshift/route.yaml
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
apiVersion: route.openshift.io/v1
kind: Route
metadata:
name: mcp-server
namespace: gps
name: gps-mcp-server
namespace: gps-mcp-server
spec:
tls:
termination: edge
to:
kind: Service
name: mcp-server
name: gps-mcp-server
port:
targetPort: http
27 changes: 19 additions & 8 deletions docs/DEPLOYMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,10 @@ Deploy GPS as a Kubernetes Service. Sessions connect via HTTP — useful when ma
apiVersion: v1
kind: Service
metadata:
name: gps-mcp
name: gps-mcp-server
spec:
selector:
app: mcp-server
app: gps-mcp-server
ports:
- port: 8000
targetPort: 8000
Expand All @@ -116,7 +116,7 @@ Point sessions at it:
"mcpServers": {
"gps": {
"type": "streamable-http",
"url": "http://gps-mcp.gps.svc:8000/mcp"
"url": "http://gps-mcp-server.gps-mcp-server.svc:8000/mcp"
}
}
}
Expand All @@ -141,10 +141,10 @@ Build and run:
uv run scripts/build_db.py --force

# Build container image
docker build -f Containerfile -t gps .
docker build --provenance=false --platform=linux/amd64 -f Containerfile -t gps-mcp-server .

# Run
docker run -p 8000:8000 gps
docker run -p 8000:8000 gps-mcp-server
```

The container exposes HTTP transport on port 8000 with a health check at `/health`.
Expand All @@ -171,6 +171,12 @@ deploy/deploy.sh status

# View logs
deploy/deploy.sh logs

# Build and push to OpenShift internal registry
IMAGE_REGISTRY=<registry-route> deploy/deploy.sh image --overlay openshift

# Full pipeline: build db + push image + deploy
IMAGE_REGISTRY=<registry-route> deploy/deploy.sh all --overlay openshift
```

### Overlays
Expand All @@ -186,10 +192,15 @@ deploy/deploy.sh apply --overlay openshift

### Environment

Set `NAMESPACE` to deploy to a specific namespace:
| Variable | Purpose |
|----------|---------|
| `NAMESPACE` | Deploy to a specific namespace |
| `IMAGE_REGISTRY` | Override container registry host (e.g., OpenShift internal registry route) |
| `IMAGE` | Override full image reference for apply |

```bash
NAMESPACE=my-gps deploy/deploy.sh apply
IMAGE_REGISTRY=default-route-openshift-image-registry.apps.example.com deploy/deploy.sh image --overlay openshift
```

## MCP Client Configuration
Expand Down Expand Up @@ -237,10 +248,10 @@ The `deploy.yml` workflow (manual dispatch only) handles the full release pipeli
# Ensure secrets are configured, then:
gh workflow run deploy.yml -f bump_type=minor
gh run watch
oc get pods -n gps
oc get pods -n gps-mcp-server
```

The workflow automatically creates a `quay-pull-secret` in the `gps` namespace from the Quay credentials.
The workflow automatically creates a `quay-pull-secret` in the `gps-mcp-server` namespace from the Quay credentials.

## Security Notes

Expand Down