Compare commits
No commits in common. "6d0454436831812eacd81bb9149c1afcbb506c29" and "9bbe9e38aecf232acba11665f834bb2f09d111c1" have entirely different histories.
6d04544368
...
9bbe9e38ae
6 changed files with 6 additions and 169 deletions
|
|
@ -560,16 +560,10 @@ def cmd_status(_args: argparse.Namespace) -> None:
|
||||||
print(f"API: {health.get('status', '?')} DB: {health.get('db', '?')}")
|
print(f"API: {health.get('status', '?')} DB: {health.get('db', '?')}")
|
||||||
summary = _api_get("/state/summary")
|
summary = _api_get("/state/summary")
|
||||||
t = summary["totals"]
|
t = summary["totals"]
|
||||||
topics = t.get("topics", {})
|
print(f"Topics: {t['topics']['active']} active")
|
||||||
workplans = t.get("workplans") or t.get("workstreams", {})
|
print(f"Workstreams: {t['workstreams']['active']} active, {t['workstreams']['blocked']} blocked")
|
||||||
tasks = t.get("tasks", {})
|
print(f"Tasks: {t['tasks']['in_progress']} in-progress, {t['tasks']['todo']} todo, {t['tasks']['blocked']} blocked")
|
||||||
decisions = t.get("decisions", {})
|
print(f"Decisions: {t['decisions']['open']} open, {t['decisions']['escalated']} escalated")
|
||||||
print(f"API base: {API_BASE}")
|
|
||||||
print(f"Topics: {topics.get('active', 0)} active")
|
|
||||||
print(f"Workplans: {workplans.get('active', 0)} active, {workplans.get('blocked', 0)} blocked")
|
|
||||||
# Task statuses are wait|todo|progress|done|cancel (see workplan-convention.md).
|
|
||||||
print(f"Tasks: {tasks.get('progress', 0)} in-progress, {tasks.get('todo', 0)} todo, {tasks.get('wait', 0)} waiting")
|
|
||||||
print(f"Decisions: {decisions.get('open', 0)} open, {decisions.get('escalated', 0)} escalated")
|
|
||||||
blocking = summary.get("blocking_decisions", [])
|
blocking = summary.get("blocking_decisions", [])
|
||||||
if blocking:
|
if blocking:
|
||||||
print(f"\nBlocking decisions ({len(blocking)}):")
|
print(f"\nBlocking decisions ({len(blocking)}):")
|
||||||
|
|
|
||||||
|
|
@ -24,31 +24,3 @@ app: {{ include "statehub.fullname" . }}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
{{- printf "%s:%s" .Values.image.repository .Values.image.tag -}}
|
{{- printf "%s:%s" .Values.image.repository .Values.image.tag -}}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
|
||||||
{{- define "statehub.mcpFullname" -}}
|
|
||||||
{{- printf "%s-mcp" (include "statehub.fullname" .) | trunc 63 | trimSuffix "-" -}}
|
|
||||||
{{- end -}}
|
|
||||||
|
|
||||||
{{- define "statehub.mcpLabels" -}}
|
|
||||||
app: {{ include "statehub.mcpFullname" . }}
|
|
||||||
app.kubernetes.io/name: {{ include "statehub.mcpFullname" . }}
|
|
||||||
app.kubernetes.io/component: mcp
|
|
||||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
|
||||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
|
||||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
|
||||||
app.kubernetes.io/part-of: railiance-apps
|
|
||||||
helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" }}
|
|
||||||
railiance.io/layer: s5-app
|
|
||||||
{{- end -}}
|
|
||||||
|
|
||||||
{{- define "statehub.mcpSelectorLabels" -}}
|
|
||||||
app: {{ include "statehub.mcpFullname" . }}
|
|
||||||
{{- end -}}
|
|
||||||
|
|
||||||
{{- define "statehub.mcpApiBase" -}}
|
|
||||||
{{- if .Values.mcp.apiBase -}}
|
|
||||||
{{- .Values.mcp.apiBase -}}
|
|
||||||
{{- else -}}
|
|
||||||
{{- printf "http://%s:%v" (include "statehub.fullname" .) .Values.service.port -}}
|
|
||||||
{{- end -}}
|
|
||||||
{{- end -}}
|
|
||||||
|
|
|
||||||
|
|
@ -1,75 +0,0 @@
|
||||||
{{- if .Values.mcp.enabled }}
|
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: {{ include "statehub.mcpFullname" . }}
|
|
||||||
labels: {{- include "statehub.mcpLabels" . | nindent 4 }}
|
|
||||||
spec:
|
|
||||||
replicas: {{ .Values.mcp.replicaCount }}
|
|
||||||
selector:
|
|
||||||
matchLabels: {{- include "statehub.mcpSelectorLabels" . | nindent 6 }}
|
|
||||||
strategy:
|
|
||||||
type: RollingUpdate
|
|
||||||
rollingUpdate:
|
|
||||||
maxSurge: 1
|
|
||||||
maxUnavailable: 0
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels: {{- include "statehub.mcpLabels" . | nindent 8 }}
|
|
||||||
spec:
|
|
||||||
securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }}
|
|
||||||
{{- with .Values.imagePullSecrets }}
|
|
||||||
imagePullSecrets: {{- toYaml . | nindent 8 }}
|
|
||||||
{{- end }}
|
|
||||||
containers:
|
|
||||||
- name: state-hub-mcp
|
|
||||||
image: {{ include "statehub.image" . | quote }}
|
|
||||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
|
||||||
securityContext: {{- toYaml .Values.securityContext | nindent 12 }}
|
|
||||||
# The image CMD serves the API; this pod runs the MCP layer instead.
|
|
||||||
# Use -m so the WORKDIR (/app) lands on sys.path — invoking the file
|
|
||||||
# directly would put /app/mcp_server there and break its own imports.
|
|
||||||
command: ["/app/.venv/bin/python", "-m", "mcp_server.server"]
|
|
||||||
ports:
|
|
||||||
- name: sse
|
|
||||||
containerPort: {{ .Values.mcp.service.targetPort }}
|
|
||||||
protocol: TCP
|
|
||||||
env:
|
|
||||||
- name: MCP_TRANSPORT
|
|
||||||
value: {{ .Values.mcp.transport | quote }}
|
|
||||||
- name: MCP_PORT
|
|
||||||
value: {{ .Values.mcp.service.targetPort | quote }}
|
|
||||||
# A Service routes to the pod IP, so the default loopback bind
|
|
||||||
# would be unreachable.
|
|
||||||
- name: MCP_HOST
|
|
||||||
value: "0.0.0.0"
|
|
||||||
# Stateless HTTP client over the API — no database access.
|
|
||||||
- name: API_BASE
|
|
||||||
value: {{ include "statehub.mcpApiBase" . | quote }}
|
|
||||||
{{- if .Values.mcp.probes.enabled }}
|
|
||||||
readinessProbe:
|
|
||||||
tcpSocket:
|
|
||||||
port: {{ .Values.mcp.service.targetPort }}
|
|
||||||
initialDelaySeconds: {{ .Values.mcp.probes.readiness.initialDelaySeconds }}
|
|
||||||
periodSeconds: {{ .Values.mcp.probes.readiness.periodSeconds }}
|
|
||||||
timeoutSeconds: {{ .Values.mcp.probes.readiness.timeoutSeconds }}
|
|
||||||
failureThreshold: {{ .Values.mcp.probes.readiness.failureThreshold }}
|
|
||||||
livenessProbe:
|
|
||||||
tcpSocket:
|
|
||||||
port: {{ .Values.mcp.service.targetPort }}
|
|
||||||
initialDelaySeconds: {{ .Values.mcp.probes.liveness.initialDelaySeconds }}
|
|
||||||
periodSeconds: {{ .Values.mcp.probes.liveness.periodSeconds }}
|
|
||||||
timeoutSeconds: {{ .Values.mcp.probes.liveness.timeoutSeconds }}
|
|
||||||
failureThreshold: {{ .Values.mcp.probes.liveness.failureThreshold }}
|
|
||||||
{{- end }}
|
|
||||||
resources: {{- toYaml .Values.mcp.resources | nindent 12 }}
|
|
||||||
{{- with .Values.nodeSelector }}
|
|
||||||
nodeSelector: {{- toYaml . | nindent 8 }}
|
|
||||||
{{- end }}
|
|
||||||
{{- with .Values.affinity }}
|
|
||||||
affinity: {{- toYaml . | nindent 8 }}
|
|
||||||
{{- end }}
|
|
||||||
{{- with .Values.tolerations }}
|
|
||||||
tolerations: {{- toYaml . | nindent 8 }}
|
|
||||||
{{- end }}
|
|
||||||
{{- end }}
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
{{- if .Values.mcp.enabled }}
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: {{ include "statehub.mcpFullname" . }}
|
|
||||||
labels: {{- include "statehub.mcpLabels" . | nindent 4 }}
|
|
||||||
spec:
|
|
||||||
# ClusterIP only, and deliberately no Ingress: the MCP layer proxies an
|
|
||||||
# unauthenticated API and must not be reachable from outside the cluster.
|
|
||||||
type: ClusterIP
|
|
||||||
ports:
|
|
||||||
- port: {{ .Values.mcp.service.port }}
|
|
||||||
targetPort: {{ .Values.mcp.service.targetPort }}
|
|
||||||
protocol: TCP
|
|
||||||
name: sse
|
|
||||||
selector: {{- include "statehub.mcpSelectorLabels" . | nindent 4 }}
|
|
||||||
{{- end }}
|
|
||||||
|
|
@ -47,38 +47,6 @@ ingress:
|
||||||
traefik.ingress.kubernetes.io/router.tls: "true"
|
traefik.ingress.kubernetes.io/router.tls: "true"
|
||||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||||
|
|
||||||
# MCP layer (CUST-WP-0067-T08). Runs from the same image with a different
|
|
||||||
# command, as a stateless HTTP client over the API service. ClusterIP only —
|
|
||||||
# it proxies an unauthenticated API, so it must never gain an Ingress.
|
|
||||||
mcp:
|
|
||||||
enabled: false
|
|
||||||
replicaCount: 1
|
|
||||||
transport: sse
|
|
||||||
# Defaults to the in-cluster API Service; override only to point elsewhere.
|
|
||||||
apiBase: ""
|
|
||||||
service:
|
|
||||||
port: 8001
|
|
||||||
targetPort: 8001
|
|
||||||
probes:
|
|
||||||
enabled: true
|
|
||||||
liveness:
|
|
||||||
initialDelaySeconds: 30
|
|
||||||
periodSeconds: 30
|
|
||||||
timeoutSeconds: 5
|
|
||||||
failureThreshold: 3
|
|
||||||
readiness:
|
|
||||||
initialDelaySeconds: 10
|
|
||||||
periodSeconds: 10
|
|
||||||
timeoutSeconds: 5
|
|
||||||
failureThreshold: 3
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
cpu: 50m
|
|
||||||
memory: 128Mi
|
|
||||||
limits:
|
|
||||||
cpu: 500m
|
|
||||||
memory: 512Mi
|
|
||||||
|
|
||||||
probes:
|
probes:
|
||||||
enabled: true
|
enabled: true
|
||||||
path: /state/health
|
path: /state/health
|
||||||
|
|
|
||||||
|
|
@ -3212,9 +3212,4 @@ if __name__ == "__main__":
|
||||||
mcp.run(transport="stdio")
|
mcp.run(transport="stdio")
|
||||||
else:
|
else:
|
||||||
port = int(os.environ.get("MCP_PORT", "8001"))
|
port = int(os.environ.get("MCP_PORT", "8001"))
|
||||||
# Default to loopback: this server is an unauthenticated proxy over the
|
mcp.run(transport=transport, host="127.0.0.1", port=port)
|
||||||
# API, so it must not land on every interface by accident. In-cluster
|
|
||||||
# deployment sets MCP_HOST=0.0.0.0 because a Service routes to the pod
|
|
||||||
# IP, and a loopback bind is unreachable there (CUST-WP-0067-T08).
|
|
||||||
host = os.environ.get("MCP_HOST", "127.0.0.1")
|
|
||||||
mcp.run(transport=transport, host=host, port=port)
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue