Technical Reference

MCP protocol

Key protocol methods

Method Description

initialize

Client → Server handshake, exchanges capabilities

tools/list

Client discovers available tools (names, descriptions, parameters)

tools/call

Client invokes a tool by name with JSON arguments

resources/list

Client discovers available data resources (optional)

prompts/list

Client discovers prompt templates (optional)

MCPServer CR reference

apiVersion: mcp.x-k8s.io/v1alpha1
kind: MCPServer
metadata:
  name: <user-prefix>-stock-mcp
spec:
  source:
    type: ContainerImage
    containerImage:
      ref: <image-registry-url>@sha256:<digest>
  config:
    port: 8080
    path: /mcp
    storage:
      - path: /tmp
        permissions: ReadWrite
        source:
          type: EmptyDir
          emptyDir: {}
      - path: /.cache
        permissions: ReadWrite
        source:
          type: EmptyDir
          emptyDir: {}
  mcp:
    stateless: true
  runtime:
    resources:
      requests:
        cpu: "100m"
        memory: "256Mi"
      limits:
        cpu: "1"
        memory: "512Mi"
The config.storage entries are required. The operator enforces a read-only root filesystem, so yfinance (writes to /.cache) and Python (writes to /tmp) need writable EmptyDir mounts. Without them, the pod crashes with Read-only file system.

Status conditions

Condition Meaning

Ready=True

Deployment running, handshake successful, service reachable

Ready=False / DeploymentUnavailable

Pods not ready — check events and pod logs

Ready=False / HandshakeFailed

Server did not respond to MCP initialize — check port, path, and server startup

What the operator creates

When you apply an MCPServer CR, the MCP Lifecycle Operator creates:

  • Deployment — security-hardened (non-root UID, drop ALL capabilities, read-only root filesystem, seccomp RuntimeDefault)

  • Service — ClusterIP targeting the configured port

  • NetworkPolicy — restricts ingress to the service port

All owned resources are garbage-collected when the MCPServer CR is deleted.

MCP Gateway (RHCL) reference

HTTPRoute

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: <user-prefix>-stock-mcp
spec:
  parentRefs:
    - name: mcp-gateway
      namespace: mcp-system
      kind: Gateway
  hostnames:
    - <gateway-hostname>
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /mcp/<user-prefix>-stock
      backendRefs:
        - name: <user-prefix>-stock-mcp
          port: 8080

MCPServerRegistration

apiVersion: mcp.kuadrant.io/v1alpha1
kind: MCPServerRegistration
metadata:
  name: <user-prefix>-stock-mcp
spec:
  prefix: <user-prefix>_stock    # <-- field is 'prefix', NOT 'toolPrefix'
  targetRef:
    group: gateway.networking.k8s.io
    kind: HTTPRoute
    name: <user-prefix>-stock-mcp
The field is prefix, not toolPrefix. Using toolPrefix is silently pruned and has no effect.

The targetRef references the HTTPRoute (not the MCPServer directly). The gateway controller discovers the backend service from the route.

Gateway class

The MCP Gateway uses data-science-gateway-class (Istio/Sail-backed), part of RHCL (Red Hat Connectivity Link) 1.4+. The MCPServerRegistration CRD is mcp.kuadrant.io/v1alpha1, distinct from the MCPServer CRD (mcp.x-k8s.io/v1alpha1) which comes from the RHOAI MCP Lifecycle Operator.

Python SDK v2 patterns

Server setup

from mcp.server import MCPServer

server = MCPServer(
    "stock-market",
    instructions="Provides comprehensive financial data for stocks.",
)

Tool definition

@server.tool(
    name="get_stock_info",
    description="Get comprehensive stock information including price, metrics, and company details.",
)
async def get_stock_info(ticker: str) -> str:
    """Get stock information for a given ticker symbol.

    Args:
        ticker: Stock ticker symbol (e.g., AAPL, MSFT, GOOGL)
    """
    try:
        import yfinance as yf
        company = yf.Ticker(ticker)
        info = company.info
        if not info or "symbol" not in info:
            return f"No stock info found for ticker {ticker}."
        return json.dumps(info)
    except Exception as e:
        return f"Error: getting stock information for {ticker}: {e}"

Key patterns:

  • @server.tool(name=…​, description=…​) — explicit name and description for the LLM

  • Type hints on all parameters — str → "type": "string", int → "type": "integer", float | None → optional number

  • Parameters without defaults become required in the JSON Schema

  • async def — enables concurrent request handling via asyncio

  • Return type is always str — tool results go into result.content[0].text in the MCP response

  • Wrap logic in try/except — return error strings, never raise exceptions from tools

Running the server (dual transport)

import os

def main() -> None:
    transport = os.environ.get("MCP_TRANSPORT", "stdio")
    if transport == "streamable-http":
        server.run(transport="streamable-http", host="0.0.0.0", port=8080)
    else:
        server.run(transport="stdio")

if __name__ == "__main__":
    main()
For container deployments, host must be "0.0.0.0" (not the default "127.0.0.1"), otherwise the server is unreachable from outside the pod. Set MCP_TRANSPORT=streamable-http in the Dockerfile via ENV.

Useful oc commands

# Check MCPServer status
oc get mcpserver

# Describe for events and conditions
oc describe mcpserver <name>

# Check build logs
oc logs bc/<name>

# Check server pod logs
oc logs deployment/<name>

# Check MCP Gateway registration
oc get mcpserverregistration

# Check HTTPRoute attachment
oc describe httproute <name>

# Quick MCP health check (from within the cluster)
curl -s http://<service-name>:8080/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}},"id":1}'