From 6e9ae078694c23d8adb2e616abe65d2693d62ae4 Mon Sep 17 00:00:00 2001 From: Andrew Stoltz <1578013+astoltz@users.noreply.github.com> Date: Tue, 16 Jun 2026 21:01:52 -0500 Subject: [PATCH] deploy: add MCP gateway for Agent Zero --- apps/agent-zero/agent-zero.yaml | 58 +++--- apps/fc-gateway/fc-gateway.yaml | 345 ++++++++++++++++++++++++++++++++ apps/telephony/telephony.yaml | 82 +++++--- 3 files changed, 431 insertions(+), 54 deletions(-) create mode 100644 apps/fc-gateway/fc-gateway.yaml diff --git a/apps/agent-zero/agent-zero.yaml b/apps/agent-zero/agent-zero.yaml index 56f9c9d..c9b0ee4 100644 --- a/apps/agent-zero/agent-zero.yaml +++ b/apps/agent-zero/agent-zero.yaml @@ -153,6 +153,17 @@ metadata: spec: itemPath: "vaults/IAmWorkin/items/FlowerCore DMS MCP Keys" +--- +# FlowerCore MCP Gateway key. Agent Zero advertises only fc_gateway so product +# tool schemas are discovered on demand instead of dumped into the prompt. +apiVersion: onepassword.com/v1 +kind: OnePasswordItem +metadata: + name: gateway-mcp-keys + namespace: agent-zero +spec: + itemPath: "vaults/IAmWorkin/items/FlowerCore Gateway MCP Keys" + --- apiVersion: apps/v1 kind: Deployment @@ -266,31 +277,11 @@ spec: MODELCFG # Strip heredoc indentation sed -i 's/^ //' /a0/usr/plugins/_model_config/config.json - # Phase 0 Chat MCP pilot: Agent Zero does not interpolate env vars - # inside A0_SET_mcp_servers JSON, so build the final JSON here from - # the secret-backed env vars before initialize.sh. Keep the local - # corpus_search.py tool mounted either way so outage fallback - # remains available even when fc_knowledge is not advertised. - export KNOWLEDGE_MCP_ENABLED=false - if [ -n "${KNOWLEDGE_MCP_BEARER_TOKEN:-}" ]; then - if curl -sf --connect-timeout 3 "${KNOWLEDGE_MCP_HEALTH_URL}" > /dev/null && \ - curl -sf --connect-timeout 5 \ - -H "Authorization: Bearer ${KNOWLEDGE_MCP_BEARER_TOKEN}" \ - -H "Accept: application/json, text/event-stream" \ - -H "Content-Type: application/json" \ - -d '{"jsonrpc":"2.0","id":"fc-knowledge-bootstrap","method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"agent-zero-bootstrap","version":"1.0"}}}' \ - "${KNOWLEDGE_MCP_URL}" > /dev/null; then - export KNOWLEDGE_MCP_ENABLED=true - echo "fc_knowledge enabled from ${KNOWLEDGE_MCP_URL}." - else - echo "fc_knowledge unavailable or unauthorized; keeping local corpus_search.py as the fallback path." - fi - else - echo "fc_knowledge token missing; keeping local corpus_search.py as the fallback path." - fi - + # Agent Zero does not interpolate env vars inside + # A0_SET_mcp_servers JSON, so build the final JSON here from the + # secret-backed gateway key before initialize.sh. export A0_SET_mcp_servers="$( - python3 -c 'import json, os; servers = {}; chat_key = os.getenv("CHAT_MCP_API_KEY"); knowledge_enabled = os.getenv("KNOWLEDGE_MCP_ENABLED", "false").lower() == "true"; token = os.getenv("KNOWLEDGE_MCP_BEARER_TOKEN", "") if knowledge_enabled else ""; chat_key and servers.setdefault("fc_chat", {"type": "streamable-http", "url": "http://chat-web.fc-chat.svc/mcp", "headers": {"X-Api-Key": chat_key}}); token and servers.setdefault("fc_knowledge", {"type": "streamable-http", "url": os.getenv("KNOWLEDGE_MCP_URL", "http://knowledge-web.knowledge.svc/mcp"), "headers": {"Authorization": f"Bearer {token}"}}); dms_key = os.getenv("DMS_MCP_API_KEY"); dms_key and servers.setdefault("fc_dms", {"type": "streamable-http", "url": os.getenv("DMS_MCP_URL", "http://dms-web.fc-dms.svc/mcp"), "headers": {"X-Api-Key": dms_key}}); print(json.dumps({"mcpServers": servers}, separators=(",", ":")))' + python3 -c 'import json, os; key=os.getenv("GATEWAY_MCP_API_KEY"); url=os.getenv("GATEWAY_MCP_URL", "http://fc-gateway.fc-gateway.svc/mcp"); servers={"fc_gateway":{"type":"streamable-http","url":url,"headers":{"X-Api-Key":key}}} if key else {}; print(json.dumps({"mcpServers": servers}, separators=(",", ":")))' )" # Run the original entrypoint exec /exe/initialize.sh $BRANCH @@ -372,6 +363,14 @@ spec: value: "http://fc-llm-bridge.fc-llm-bridge.svc:8080" - name: FLOWERCORE_AGENTZERO_OLLAMA_URL value: "http://fc-llm-bridge.fc-llm-bridge.svc:8080" + # FlowerCore.Mcp.Gateway — single MCP fan-in for product tools. + - name: GATEWAY_MCP_URL + value: "http://fc-gateway.fc-gateway.svc/mcp" + - name: GATEWAY_MCP_API_KEY + valueFrom: + secretKeyRef: + name: gateway-mcp-keys + key: agent-zero # Agent profile — Blue Jay personality, tools, and system prompt - name: A0_SET_agent_profile value: "bluejay" @@ -678,6 +677,17 @@ spec: protocol: TCP - port: 8080 protocol: TCP + # FlowerCore.Mcp.Gateway — Agent Zero advertises this single MCP server; + # the gateway performs backend search and invokes product MCP tools. + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: fc-gateway + ports: + - port: 80 + protocol: TCP + - port: 8080 + protocol: TCP # Allow internet (for kubectl image pull, etc) - to: - ipBlock: diff --git a/apps/fc-gateway/fc-gateway.yaml b/apps/fc-gateway/fc-gateway.yaml new file mode 100644 index 0000000..2a0dea5 --- /dev/null +++ b/apps/fc-gateway/fc-gateway.yaml @@ -0,0 +1,345 @@ +# FlowerCore.Mcp.Gateway — Agent Zero MCP tool-router. +# Exposes one AZ-facing MCP endpoint with three meta-tools: +# list_capabilities, search_tools, invoke_tool. +--- +apiVersion: v1 +kind: Namespace +metadata: + name: fc-gateway + labels: + app.kubernetes.io/part-of: flowercore +--- +# Inbound key Agent Zero sends to the gateway as X-Api-Key. +apiVersion: onepassword.com/v1 +kind: OnePasswordItem +metadata: + name: gateway-mcp-keys + namespace: fc-gateway +spec: + itemPath: "vaults/IAmWorkin/items/FlowerCore Gateway MCP Keys" +--- +# Embedding API key for fc:embedding via fc-llm-bridge. Optional at runtime: +# the gateway falls back to deterministic keyword ranking if embeddings are down. +apiVersion: onepassword.com/v1 +kind: OnePasswordItem +metadata: + name: fc-llm-bridge-api-keys + namespace: fc-gateway +spec: + itemPath: "vaults/IAmWorkin/items/FC LLM Bridge API Keys" +--- +apiVersion: onepassword.com/v1 +kind: OnePasswordItem +metadata: + name: mysql-mcp-keys + namespace: fc-gateway +spec: + itemPath: "vaults/IAmWorkin/items/FlowerCore MySQL MCP Keys" +--- +apiVersion: onepassword.com/v1 +kind: OnePasswordItem +metadata: + name: telephony-mcp-keys + namespace: fc-gateway +spec: + itemPath: "vaults/IAmWorkin/items/Twilio IVR MCP Token (Agent Zero)" +--- +apiVersion: onepassword.com/v1 +kind: OnePasswordItem +metadata: + name: chat-mcp-keys + namespace: fc-gateway +spec: + itemPath: "vaults/IAmWorkin/items/FlowerCore Chat MCP Keys" +--- +apiVersion: onepassword.com/v1 +kind: OnePasswordItem +metadata: + name: dms-mcp-keys + namespace: fc-gateway +spec: + itemPath: "vaults/IAmWorkin/items/FlowerCore DMS MCP Keys" +--- +apiVersion: onepassword.com/v1 +kind: OnePasswordItem +metadata: + name: knowledge-mcp-tokens + namespace: fc-gateway +spec: + itemPath: "vaults/IAmWorkin/items/FlowerCore Knowledge MCP Tokens" +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: fc-gateway + namespace: fc-gateway + labels: + app.kubernetes.io/name: fc-gateway + app.kubernetes.io/part-of: flowercore +spec: + replicas: 1 + revisionHistoryLimit: 3 + strategy: + type: Recreate + selector: + matchLabels: + app.kubernetes.io/name: fc-gateway + template: + metadata: + labels: + app.kubernetes.io/name: fc-gateway + app.kubernetes.io/part-of: flowercore + annotations: + fc.flowercore.io/healthz-anon: "true" + fc.flowercore.io/probe-path: "/healthz" + prometheus.io/scrape: "true" + prometheus.io/port: "8080" + prometheus.io/path: "/metrics/prometheus" + spec: + securityContext: + runAsNonRoot: true + fsGroup: 1654 + fsGroupChangePolicy: OnRootMismatch + containers: + - name: web + image: localhost/fc-gateway:v20260617-az-gateway + imagePullPolicy: Never + ports: + - containerPort: 8080 + name: http + env: + - name: ASPNETCORE_URLS + value: "http://+:8080" + - name: ASPNETCORE_ENVIRONMENT + value: "Production" + - name: DOTNET_SYSTEM_GLOBALIZATION_INVARIANT + value: "false" + - name: FlowerCore__Mcp__ApiKey__Key + valueFrom: + secretKeyRef: + name: gateway-mcp-keys + key: agent-zero + - name: FlowerCore__Mcp__Gateway__Embedding__BaseUrl + value: "http://fc-llm-bridge.fc-llm-bridge.svc:8080/v1" + - name: FlowerCore__Mcp__Gateway__Embedding__Model + value: "fc:embedding" + - name: FlowerCore__Mcp__Gateway__Embedding__Mode + value: "openai" + - name: FlowerCore__Mcp__Gateway__Embedding__ApiKey + valueFrom: + secretKeyRef: + name: fc-llm-bridge-api-keys + key: agent-zero-k8s + optional: true + - name: GW_BACKEND_fc_mysql_KEY + valueFrom: + secretKeyRef: + name: mysql-mcp-keys + key: credential + optional: true + - name: GW_BACKEND_fc_telephony_KEY + valueFrom: + secretKeyRef: + name: telephony-mcp-keys + key: credential + optional: true + - name: GW_BACKEND_fc_chat_KEY + valueFrom: + secretKeyRef: + name: chat-mcp-keys + key: credential + optional: true + - name: GW_BACKEND_fc_dms_KEY + valueFrom: + secretKeyRef: + name: dms-mcp-keys + key: credential + optional: true + - name: GW_BACKEND_fc_knowledge_KEY + valueFrom: + secretKeyRef: + name: knowledge-mcp-tokens + key: password + optional: true + resources: + requests: + cpu: 50m + memory: 128Mi + limits: + cpu: 500m + memory: 384Mi + volumeMounts: + - name: tmp + mountPath: /tmp + - name: logs + mountPath: /home/app/logs + securityContext: + runAsNonRoot: true + runAsUser: 1654 + runAsGroup: 1654 + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: [ALL] + startupProbe: + httpGet: + path: /healthz + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 5 + failureThreshold: 30 + readinessProbe: + httpGet: + path: /healthz + port: 8080 + periodSeconds: 10 + livenessProbe: + tcpSocket: + port: 8080 + initialDelaySeconds: 30 + periodSeconds: 30 + volumes: + - name: tmp + emptyDir: {} + - name: logs + emptyDir: {} +--- +apiVersion: v1 +kind: Service +metadata: + name: fc-gateway + namespace: fc-gateway +spec: + type: ClusterIP + selector: + app.kubernetes.io/name: fc-gateway + ports: + - name: http + port: 80 + targetPort: 8080 + protocol: TCP +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: fc-gateway-tls + namespace: fc-gateway +spec: + secretName: fc-gateway-tls + issuerRef: + name: step-ca-acme + kind: ClusterIssuer + dnsNames: + - gateway.iamworkin.lan + duration: 720h + renewBefore: 240h +--- +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: fc-gateway + namespace: fc-gateway +spec: + entryPoints: + - websecure + routes: + - match: Host(`gateway.iamworkin.lan`) + kind: Rule + services: + - name: fc-gateway + port: 80 + tls: + secretName: fc-gateway-tls +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: fc-gateway-netpol + namespace: fc-gateway +spec: + podSelector: + matchLabels: + app.kubernetes.io/name: fc-gateway + policyTypes: + - Ingress + - Egress + ingress: + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: agent-zero + ports: + - port: 8080 + protocol: TCP + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: traefik-system + ports: + - port: 8080 + protocol: TCP + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: monitoring + ports: + - port: 8080 + protocol: TCP + egress: + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + ports: + - port: 53 + protocol: UDP + - port: 53 + protocol: TCP + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: fc-llm-bridge + ports: + - port: 8080 + protocol: TCP + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: fc-mysql + ports: + - port: 5300 + protocol: TCP + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: telephony + ports: + - port: 5100 + protocol: TCP + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: fc-chat + ports: + - port: 80 + protocol: TCP + - port: 8080 + protocol: TCP + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: fc-dms + ports: + - port: 80 + protocol: TCP + - port: 8080 + protocol: TCP + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: knowledge + ports: + - port: 80 + protocol: TCP + - port: 8080 + protocol: TCP diff --git a/apps/telephony/telephony.yaml b/apps/telephony/telephony.yaml index 232e65b..3940067 100644 --- a/apps/telephony/telephony.yaml +++ b/apps/telephony/telephony.yaml @@ -25,17 +25,25 @@ data: --- # 1Password → K8s Secret sync for Twilio credentials # Creates secret "twilio-credentials" with fields: AccountSid, AuthToken, DefaultFromNumber -apiVersion: onepassword.com/v1 -kind: OnePasswordItem -metadata: - name: twilio-credentials - namespace: telephony -spec: - itemPath: "vaults/IAmWorkin/items/Twilio Account" ---- -# Application configuration overlay -apiVersion: v1 -kind: ConfigMap +apiVersion: onepassword.com/v1 +kind: OnePasswordItem +metadata: + name: twilio-credentials + namespace: telephony +spec: + itemPath: "vaults/IAmWorkin/items/Twilio Account" +--- +apiVersion: onepassword.com/v1 +kind: OnePasswordItem +metadata: + name: telephony-mcp-keys + namespace: telephony +spec: + itemPath: "vaults/IAmWorkin/items/Twilio IVR MCP Token (Agent Zero)" +--- +# Application configuration overlay +apiVersion: v1 +kind: ConfigMap metadata: name: telephony-config namespace: telephony @@ -180,14 +188,20 @@ spec: name: twilio-credentials key: AuthToken optional: true - - name: Telephony__Twilio__DefaultFromNumber - valueFrom: - secretKeyRef: - name: twilio-credentials - key: DefaultFromNumber - optional: true - # Env vars OVERRIDE appsettings.Production.json in ASP.NET Core config. - # These were previously applied live-only (kubectl) and drifted from git; + - name: Telephony__Twilio__DefaultFromNumber + valueFrom: + secretKeyRef: + name: twilio-credentials + key: DefaultFromNumber + optional: true + - name: FlowerCore__Mcp__ApiKey__Key + valueFrom: + secretKeyRef: + name: telephony-mcp-keys + key: credential + optional: true + # Env vars OVERRIDE appsettings.Production.json in ASP.NET Core config. + # These were previously applied live-only (kubectl) and drifted from git; # codified here so git is the source of truth. Tts__PiperUrl is the real # TTS cutover lever (the configmap "Tts" block is shadowed by this env). - name: Tts__PiperUrl @@ -301,17 +315,25 @@ spec: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: traefik-system - # Allow Selenium Grid for automated UI testing - - from: - - namespaceSelector: - matchLabels: - kubernetes.io/metadata.name: selenium - ports: - - port: 5100 - protocol: TCP - # Allow SIP/RTP from external sources (Yealink phones, Twilio SIP trunk) - - from: - - ipBlock: + # Allow Selenium Grid for automated UI testing + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: selenium + ports: + - port: 5100 + protocol: TCP + # Allow FlowerCore.Mcp.Gateway to reach Telephony /mcp on the destination pod port. + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: fc-gateway + ports: + - port: 5100 + protocol: TCP + # Allow SIP/RTP from external sources (Yealink phones, Twilio SIP trunk) + - from: + - ipBlock: cidr: 0.0.0.0/0 ports: - port: 5060