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
8 changes: 5 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -329,9 +329,11 @@ curl -i -H "content-type: application/json" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"add","arguments":{"a":2,"b":3}}}' $BASE
```

If a just-created `MCPAgentSession` returns `session_not_found`, first confirm
`server policy inspect` shows the session, then allow a few seconds for the
`mcp-gateway` sidecar to reload its mounted policy file.
If you just applied `MCPAccessGrant` or `MCPAgentSession` resources, remember
that `server policy inspect` only confirms the rendered policy. The proxy
sidecar reloads its local policy file on a short polling loop, so allow a few
seconds before concluding a fresh session-backed request failed with
`session_not_found`.

**Bulk (Python)** — fires many `tools/call` events for ingest testing:

Expand Down
7 changes: 6 additions & 1 deletion docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,8 @@ until ./bin/mcp-runtime server policy inspect go-example-mcp --namespace mcp-ser
sleep 2
done

# The proxy sidecar reloads the rendered policy file on a short polling loop.
# The proxy sidecar reloads rendered policy on a short polling loop, so give the
# gateway a few seconds to observe the new access session before the first tool call.
sleep 6
```

Expand Down Expand Up @@ -358,6 +359,10 @@ curl -sS \
You should see successful `tools/call` responses containing `5` and
`HELLO WORLD`. Then verify Sentinel health and query the analytics API:

The bundled Go example server also exposes `upper`, `lower`, `echo`, and
`slugify`, and each of those tools expects a `message` field in `arguments`
instead of `input` or `text`.

```bash
./bin/mcp-runtime sentinel status
./bin/mcp-runtime sentinel events
Expand Down
2 changes: 1 addition & 1 deletion internal/cli/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -599,7 +599,7 @@ metadata:
name: %s
`, name)
// #nosec G204 -- fixed kubectl command, input via stdin; name from internal code.
cmd, err := m.kubectl.CommandArgs([]string{"apply", "-f", "-"})
cmd, err := m.kubectl.CommandArgs([]string{"apply", "--validate=false", "-f", "-"})
if err != nil {
return err
}
Expand Down
19 changes: 14 additions & 5 deletions internal/operator/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ func (r *MCPServerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
return ctrl.Result{Requeue: false}, err
}

phase, allReady := determinePhase(readiness)
phase, allReady := determinePhase(readiness, mcpServer)
r.updateStatus(ctx, mcpServer, phase, "All resources reconciled", readiness)

logger.Info("Successfully reconciled MCPServer", "name", mcpServer.Name, "phase", phase)
Expand Down Expand Up @@ -299,10 +299,13 @@ func (r *MCPServerReconciler) checkResourceReadiness(ctx context.Context, mcpSer
return resourceReadiness{}, err
}

gatewayReady := true
gatewayReady := false
if gatewayEnabled(mcpServer) {
gatewayReady = deploymentReady
}
Comment on lines +302 to 305
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For consistency with gatewayReady and policyReady, canaryReady should also be set to false when the canary feature is disabled. This ensures that the status correctly reflects that the feature is not in use and prevents a logic bug in determinePhase where a disabled canary (which currently defaults to true in checkCanaryDeploymentReady) could cause the server to report PartiallyReady instead of Pending when no other resources are ready.

gatewayReady := false
if gatewayEnabled(mcpServer) {
	gatewayReady = deploymentReady
}

if !canaryEnabled(mcpServer) {
	canaryReady = false
}

if !canaryEnabled(mcpServer) {
canaryReady = false
}

return resourceReadiness{
Deployment: deploymentReady,
Expand All @@ -314,8 +317,14 @@ func (r *MCPServerReconciler) checkResourceReadiness(ctx context.Context, mcpSer
}, nil
}

func determinePhase(readiness resourceReadiness) (string, bool) {
allReady := readiness.Deployment && readiness.Service && readiness.Ingress && readiness.Gateway && readiness.Policy && readiness.Canary
func determinePhase(readiness resourceReadiness, mcpServer *mcpv1alpha1.MCPServer) (string, bool) {
allReady := readiness.Deployment && readiness.Service && readiness.Ingress
if gatewayEnabled(mcpServer) {
allReady = allReady && readiness.Gateway && readiness.Policy
}
if canaryEnabled(mcpServer) {
allReady = allReady && readiness.Canary
}
if allReady {
return "Ready", true
}
Expand Down Expand Up @@ -993,7 +1002,7 @@ func canaryEnabled(mcpServer *mcpv1alpha1.MCPServer) bool {

func (r *MCPServerReconciler) checkPolicyConfigMapReady(ctx context.Context, mcpServer *mcpv1alpha1.MCPServer) (bool, error) {
if !gatewayEnabled(mcpServer) {
return true, nil
return false, nil
}
configMap := &corev1.ConfigMap{}
if err := r.Get(ctx, types.NamespacedName{Name: gatewayPolicyConfigMapName(mcpServer.Name), Namespace: mcpServer.Namespace}, configMap); err != nil {
Expand Down
21 changes: 18 additions & 3 deletions internal/operator/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -837,8 +837,8 @@ func TestCheckResourceReadiness(t *testing.T) {
assertEqual(t, "deploymentReady", readiness.Deployment, false)
assertEqual(t, "serviceReady", readiness.Service, false)
assertEqual(t, "ingressReady", readiness.Ingress, false)
assertEqual(t, "policyReady", readiness.Policy, true)
assertEqual(t, "canaryReady", readiness.Canary, true)
assertEqual(t, "policyReady", readiness.Policy, false)
assertEqual(t, "canaryReady", readiness.Canary, false)
})
}

Expand Down Expand Up @@ -948,17 +948,32 @@ func TestUpdateStatus(t *testing.T) {

func TestDeterminePhase(t *testing.T) {
t.Run("succeeds with valid phase", func(t *testing.T) {
gatewayDisabledMCP := &mcpv1alpha1.MCPServer{}
phase, allReady := determinePhase(resourceReadiness{
Deployment: true,
Service: true,
Ingress: true,
Gateway: true,
Policy: true,
Canary: true,
})
}, gatewayDisabledMCP)
assertEqual(t, "phase", phase, "Ready")
assertEqual(t, "allReady", allReady, true)
})

t.Run("returns pending when optional resources are disabled and core resources are not ready", func(t *testing.T) {
gatewayDisabledMCP := &mcpv1alpha1.MCPServer{}
phase, allReady := determinePhase(resourceReadiness{
Deployment: false,
Service: false,
Ingress: false,
Gateway: false,
Policy: false,
Canary: false,
}, gatewayDisabledMCP)
assertEqual(t, "phase", phase, "Pending")
assertEqual(t, "allReady", allReady, false)
})
}

func TestCheckDeploymentReady(t *testing.T) {
Expand Down
85 changes: 9 additions & 76 deletions test/e2e/kind.sh
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,6 @@ RUST_EXAMPLE_SERVER_ROUTE="${RUST_EXAMPLE_SERVER_ROUTE:-/${RUST_EXAMPLE_SERVER_N
GO_EXAMPLE_SERVER_NAME="${GO_EXAMPLE_SERVER_NAME:-go-example-mcp}"
GO_EXAMPLE_SERVER_HOST="${GO_EXAMPLE_SERVER_HOST:-${PLATFORM_HOST}}"
GO_EXAMPLE_SERVER_ROUTE="${GO_EXAMPLE_SERVER_ROUTE:-/${GO_EXAMPLE_SERVER_NAME}/mcp}"
SHARED_SDK_HOST="${SHARED_SDK_HOST:-${PLATFORM_HOST}}"
PYTHON_SHARED_SERVER_NAME="${PYTHON_SHARED_SERVER_NAME:-python-shared-mcp}"
PYTHON_SHARED_SERVER_ROUTE="${PYTHON_SHARED_SERVER_ROUTE:-/${PYTHON_SHARED_SERVER_NAME}/mcp}"
RUST_SHARED_SERVER_NAME="${RUST_SHARED_SERVER_NAME:-rust-shared-mcp}"
RUST_SHARED_SERVER_ROUTE="${RUST_SHARED_SERVER_ROUTE:-/${RUST_SHARED_SERVER_NAME}/mcp}"
GO_SHARED_SERVER_NAME="${GO_SHARED_SERVER_NAME:-go-shared-mcp}"
GO_SHARED_SERVER_ROUTE="${GO_SHARED_SERVER_ROUTE:-/${GO_SHARED_SERVER_NAME}/mcp}"
HUMAN_ID="${HUMAN_ID:-user-123}"
AGENT_ID="${AGENT_ID:-ops-agent}"
SESSION_ID="${SESSION_ID:-sess-ops-agent}"
Expand All @@ -69,10 +62,7 @@ OAUTH_PROXY_PORT="${OAUTH_PROXY_PORT:-18096}"
OAUTH_UPSTREAM_PORT="${OAUTH_UPSTREAM_PORT:-18097}"
PYTHON_EXAMPLE_PROXY_PORT="${PYTHON_EXAMPLE_PROXY_PORT:-18098}"
RUST_EXAMPLE_PROXY_PORT="${RUST_EXAMPLE_PROXY_PORT:-18099}"
PYTHON_SHARED_PROXY_PORT="${PYTHON_SHARED_PROXY_PORT:-18100}"
RUST_SHARED_PROXY_PORT="${RUST_SHARED_PROXY_PORT:-18101}"
GO_EXAMPLE_PROXY_PORT="${GO_EXAMPLE_PROXY_PORT:-18102}"
GO_SHARED_PROXY_PORT="${GO_SHARED_PROXY_PORT:-18103}"
API_METRICS_PORT="${API_METRICS_PORT:-19090}"
INGEST_METRICS_PORT="${INGEST_METRICS_PORT:-19091}"
PROCESSOR_METRICS_PORT="${PROCESSOR_METRICS_PORT:-19092}"
Expand Down Expand Up @@ -1213,19 +1203,19 @@ wait_for_named_server_ready() {
local i
for i in $(seq 1 "${tries}"); do
local deployment_ready
local gateway_ready
local policy_ready
local phase
local service_ready
local ingress_ready
deployment_ready="$(kubectl get mcpserver "${server_name}" -n "${namespace}" -o jsonpath='{.status.deploymentReady}' 2>/dev/null || true)"
gateway_ready="$(kubectl get mcpserver "${server_name}" -n "${namespace}" -o jsonpath='{.status.gatewayReady}' 2>/dev/null || true)"
policy_ready="$(kubectl get mcpserver "${server_name}" -n "${namespace}" -o jsonpath='{.status.policyReady}' 2>/dev/null || true)"
service_ready="$(kubectl get mcpserver "${server_name}" -n "${namespace}" -o jsonpath='{.status.serviceReady}' 2>/dev/null || true)"
if [[ "${deployment_ready}" == "true" && "${gateway_ready}" == "true" && "${policy_ready}" == "true" && "${service_ready}" == "true" ]]; then
ingress_ready="$(kubectl get mcpserver "${server_name}" -n "${namespace}" -o jsonpath='{.status.ingressReady}' 2>/dev/null || true)"
phase="$(kubectl get mcpserver "${server_name}" -n "${namespace}" -o jsonpath='{.status.phase}' 2>/dev/null || true)"
if [[ "${deployment_ready}" == "true" && "${service_ready}" == "true" && "${ingress_ready}" == "true" && "${phase}" == "Ready" ]]; then
return 0
fi
sleep 2
done
echo "timed out waiting for MCPServer ${server_name} to report service/deployment/gateway/policy readiness" >&2
echo "timed out waiting for MCPServer ${server_name} to report core readiness and phase Ready" >&2
kubectl get mcpserver "${server_name}" -n "${namespace}" -o yaml || true
return 1
}
Expand Down Expand Up @@ -1804,24 +1794,6 @@ deploy_example_server_via_pipeline \
"${GO_EXAMPLE_SERVER_ROUTE}" \
"${GO_EXAMPLE_SOURCE_DIR}" \
"${GO_EXAMPLE_WORKDIR}"
deploy_example_server_via_pipeline \
"${PYTHON_SHARED_SERVER_NAME}" \
"${SHARED_SDK_HOST}" \
"${PYTHON_SHARED_SERVER_ROUTE}" \
"${PYTHON_EXAMPLE_SOURCE_DIR}" \
"${WORKDIR}/python-shared-mcp-server"
deploy_example_server_via_pipeline \
"${RUST_SHARED_SERVER_NAME}" \
"${SHARED_SDK_HOST}" \
"${RUST_SHARED_SERVER_ROUTE}" \
"${RUST_EXAMPLE_SOURCE_DIR}" \
"${WORKDIR}/rust-shared-mcp-server"
deploy_example_server_via_pipeline \
"${GO_SHARED_SERVER_NAME}" \
"${SHARED_SDK_HOST}" \
"${GO_SHARED_SERVER_ROUTE}" \
"${GO_EXAMPLE_SOURCE_DIR}" \
"${WORKDIR}/go-shared-mcp-server"

echo "[cli] checking server commands"

Expand Down Expand Up @@ -1867,6 +1839,9 @@ check("deploymentReady: true" in get_yaml,
check("serviceReady: true" in get_yaml,
"serviceReady: true",
f"server get: serviceReady is not true\n{get_yaml}")
check('type: CanaryReady' in get_yaml and 'status: "False"' in get_yaml,
'CanaryReady condition is false',
f"server get: CanaryReady condition is not false for a server without canary rollout\n{get_yaml}")

# Assert spec fields reflect what was deployed
expected_path = f"/{server_name}/mcp"
Expand Down Expand Up @@ -1902,17 +1877,6 @@ print(f"[cli] Local e2e MCP client config for {server_name}:")
print(json.dumps(config, indent=2))
PYEOF

# --- server status: assert the primary server appears ---
_cli_status_out="$(./bin/mcp-runtime server status --namespace mcp-servers 2>&1)"
if ! printf '%s\n' "${_cli_status_out}" | grep -qF "${SERVER_NAME}"; then
echo "[cli][fail] 'server status' output does not contain ${SERVER_NAME}" >&2
printf '%s\n' "${_cli_status_out}" >&2
exit 1
fi
echo "[cli][pass] server status contains ${SERVER_NAME}"

./bin/mcp-runtime server logs "${SERVER_NAME}" --namespace mcp-servers >"${WORKDIR}/${SERVER_NAME}.logs"

echo "[policy] applying access grant via CLI"
cat >"${WORKDIR}/access-grant.yaml" <<EOF
apiVersion: mcpruntime.org/v1alpha1
Expand Down Expand Up @@ -2050,32 +2014,13 @@ start_header_proxy_bg "${GO_EXAMPLE_PROXY_PORT}" \
"${WORKDIR}/go-example-proxy.log" \
--host-header "${GO_EXAMPLE_SERVER_HOST}" \
--header "Mcp-Protocol-Version=${MCP_PROTOCOL_VERSION}"
start_header_proxy_bg "${PYTHON_SHARED_PROXY_PORT}" \
"http://127.0.0.1:${TRAEFIK_PORT}" \
"${WORKDIR}/python-shared-proxy.log" \
--host-header "${SHARED_SDK_HOST}" \
--header "Mcp-Protocol-Version=${MCP_PROTOCOL_VERSION}"
start_header_proxy_bg "${RUST_SHARED_PROXY_PORT}" \
"http://127.0.0.1:${TRAEFIK_PORT}" \
"${WORKDIR}/rust-shared-proxy.log" \
--host-header "${SHARED_SDK_HOST}" \
--header "Mcp-Protocol-Version=${MCP_PROTOCOL_VERSION}"
start_header_proxy_bg "${GO_SHARED_PROXY_PORT}" \
"http://127.0.0.1:${TRAEFIK_PORT}" \
"${WORKDIR}/go-shared-proxy.log" \
--host-header "${SHARED_SDK_HOST}" \
--header "Mcp-Protocol-Version=${MCP_PROTOCOL_VERSION}"

wait_port "${MCP_SMOKE_ANON_PORT}"
wait_port "${MCP_SMOKE_IDENTITY_PORT}"
wait_port "${MCP_SMOKE_SESSION_PORT}"
wait_port "${MCP_SMOKE_BAD_SESSION_PORT}"
wait_port "${PYTHON_EXAMPLE_PROXY_PORT}"
wait_port "${RUST_EXAMPLE_PROXY_PORT}"
wait_port "${GO_EXAMPLE_PROXY_PORT}"
wait_port "${PYTHON_SHARED_PROXY_PORT}"
wait_port "${RUST_SHARED_PROXY_PORT}"
wait_port "${GO_SHARED_PROXY_PORT}"

MCP_INGRESS_PATH="/${SERVER_NAME}/mcp"
MCP_DIRECT_URL="http://127.0.0.1:${TRAEFIK_PORT}${MCP_INGRESS_PATH}"
Expand All @@ -2086,18 +2031,12 @@ MCP_BAD_SESSION_URL="http://127.0.0.1:${MCP_SMOKE_BAD_SESSION_PORT}${MCP_INGRESS
PYTHON_EXAMPLE_URL="http://127.0.0.1:${PYTHON_EXAMPLE_PROXY_PORT}${PYTHON_EXAMPLE_SERVER_ROUTE}"
RUST_EXAMPLE_URL="http://127.0.0.1:${RUST_EXAMPLE_PROXY_PORT}${RUST_EXAMPLE_SERVER_ROUTE}"
GO_EXAMPLE_URL="http://127.0.0.1:${GO_EXAMPLE_PROXY_PORT}${GO_EXAMPLE_SERVER_ROUTE}"
PYTHON_SHARED_URL="http://127.0.0.1:${PYTHON_SHARED_PROXY_PORT}${PYTHON_SHARED_SERVER_ROUTE}"
RUST_SHARED_URL="http://127.0.0.1:${RUST_SHARED_PROXY_PORT}${RUST_SHARED_SERVER_ROUTE}"
GO_SHARED_URL="http://127.0.0.1:${GO_SHARED_PROXY_PORT}${GO_SHARED_SERVER_ROUTE}"

echo "[ingress] validating distinct MCP server behaviors across routes"
wait_for_mcp_tool_result "${MCP_SESSION_URL}" "aaa-ping" '{}' 200 "pong"
wait_for_mcp_tool_result "${PYTHON_EXAMPLE_URL}" "echo" '{"message":"python example ready"}' 200 "python example ready"
wait_for_mcp_tool_result "${RUST_EXAMPLE_URL}" "repeat" '{"message":"rust","times":3}' 200 "rustrustrust"
wait_for_mcp_tool_result "${GO_EXAMPLE_URL}" "lower" '{"message":"GO Example Ready"}' 200 "go example ready"
wait_for_mcp_tool_result "${PYTHON_SHARED_URL}" "reverse" '{"message":"shared host python"}' 200 "nohtyp tsoh derahs"
wait_for_mcp_tool_result "${RUST_SHARED_URL}" "word_count" '{"message":"shared host rust route"}' 200 "4"
wait_for_mcp_tool_result "${GO_SHARED_URL}" "slugify" '{"message":"Shared Host Go Route"}' 200 "shared-host-go-route"

if scenario_selected "smoke-auth"; then
echo "[mcp] validating raw MCP request edge cases"
Expand Down Expand Up @@ -3904,12 +3843,6 @@ kubectl wait --for=delete "mcpserver/${PYTHON_EXAMPLE_SERVER_NAME}" -n mcp-serve
kubectl wait --for=delete "mcpserver/${RUST_EXAMPLE_SERVER_NAME}" -n mcp-servers --timeout=120s || true
./bin/mcp-runtime server delete "${GO_EXAMPLE_SERVER_NAME}" --namespace mcp-servers
kubectl wait --for=delete "mcpserver/${GO_EXAMPLE_SERVER_NAME}" -n mcp-servers --timeout=120s || true
./bin/mcp-runtime server delete "${PYTHON_SHARED_SERVER_NAME}" --namespace mcp-servers
kubectl wait --for=delete "mcpserver/${PYTHON_SHARED_SERVER_NAME}" -n mcp-servers --timeout=120s || true
./bin/mcp-runtime server delete "${RUST_SHARED_SERVER_NAME}" --namespace mcp-servers
kubectl wait --for=delete "mcpserver/${RUST_SHARED_SERVER_NAME}" -n mcp-servers --timeout=120s || true
./bin/mcp-runtime server delete "${GO_SHARED_SERVER_NAME}" --namespace mcp-servers
kubectl wait --for=delete "mcpserver/${GO_SHARED_SERVER_NAME}" -n mcp-servers --timeout=120s || true
./bin/mcp-runtime server delete "${SERVER_NAME}" --namespace mcp-servers
kubectl wait --for=delete "mcpserver/${SERVER_NAME}" -n mcp-servers --timeout=120s || true

Expand Down
Loading