> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://help.moveworks.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://help.moveworks.com/_mcp/server.

# Build a private MCP server for MCP Workspace

> Build, secure, and test a private MCP server that connects reliably to MCP Workspace.

MCP Workspace is available to a limited set of customers. Moveworks is validating the experience and gathering feedback, so capabilities and limits may change. To request access, [register your interest](https://community.moveworks.com/p/mcp-workspace-controlled-availability).

This guide is for remote MCP servers that connect to MCP Workspace over Streamable HTTP. It focuses on the failure-prone parts: endpoint behavior, OAuth discovery, tool boundaries, and tests. It does not teach MCP from scratch or compare integration models.

## Protocol and SDKs

Follow the current MCP specification. Test your server with the MCP project's own tools:

* [Streamable HTTP transport](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports) defines the remote MCP endpoint contract.
* [Authorization](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization) defines the OAuth requirements and supported client-registration mechanisms.
* [Official MCP SDKs](https://modelcontextprotocol.io/docs/sdk) list the supported TypeScript and Python implementations.
* [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) is the first place to test connection, tool schemas, inputs, outputs, and errors.

Start with the [official TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk) or [official Python SDK](https://github.com/modelcontextprotocol/python-sdk). Use a different implementation only when you need one.

## Before you connect

Before you give an administrator an MCP Workspace endpoint, confirm:

* A stable Streamable HTTP endpoint is reachable over TLS.
* Protected Resource Metadata points to the correct authorization server.
* Authorization-server metadata advertises the authorization and token endpoints and OAuth 2.1 Authorization Code Grant with PKCE. If you want automatic setup, it also advertises Dynamic Client Registration (DCR). Include the revocation endpoint when your authorization server supports token revocation.
* The authorization server accepts the RFC 8707 `resource` parameter in authorization and token requests, then issues an access token for the canonical MCP server URI.
* A user can complete authorization, receive an access token for your MCP server, and call `tools/list` and `tools/call`.
* Your tool inventory, schemas, success results, and error responses have passed MCP Inspector tests.
* You can trace a failed request from MCP Workspace to your server logs without recording tokens or sensitive employee data.

## Remote server contract

For a remote server, use Streamable HTTP. Support both `POST` and `GET`, keep behavior stable behind the load balancer, and validate `Origin`.

```mermaid
flowchart LR
    E[Employee] -->|asks a question| M[MCP Workspace]
    M -->|OAuth authorization| A[Authorization server]
    A -->|employee access token| M
    M -->|MCP call with bearer token| S[Private MCP server]
    S -->|entitlement check and data access| P[Product APIs]
    P --> S
    S -->|focused tool result| M
```

Keep the endpoint and public tool contract stable:

* Use a production hostname, TLS, and a health-checked deployment.
* Preserve tool names, input schemas, and output schemas across releases. Add a new tool or optional field instead of silently changing an existing contract.
* Return a deterministic tool inventory for a given authenticated caller. Do not let unauthenticated or session-only state change which tools appear.
* Include a request ID in logs and support errors. Do not log tokens or raw sensitive employee data.

## OAuth 2.1, discovery, and Dynamic Client Registration

MCP Workspace uses the OAuth 2.1 Authorization Code Grant with PKCE for employee-specific data. A server that supports [Dynamic Client Registration (DCR)](https://www.rfc-editor.org/rfc/rfc7591) lets MCP Workspace complete client setup automatically. Without DCR, a developer or administrator configures the server through an HTTP connector. The access token represents the employee making the request, and your server checks that employee's upstream permissions on every call.

When an employee uses the integration for the first time, MCP Workspace sends them to your authorization server to sign in and grant consent. It does not use a shared service account or elevate the employee's permissions.

### Publish discovery metadata

A protected MCP server is an OAuth resource server. Publish [Protected Resource Metadata](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization) so the client can find its authorization server.

* Serve Protected Resource Metadata at the appropriate RFC 9728 well-known URI, or return its URL in the `resource_metadata` parameter of a `WWW-Authenticate` header on `401 Unauthorized`.
* Include at least one `authorization_servers` entry in the metadata document.
* Have the authorization server publish OAuth Authorization Server Metadata or OpenID Connect Discovery. The metadata must advertise the authorization and token endpoints and `S256` PKCE support. Add a DCR `registration_endpoint` for automatic client setup. Include the revocation endpoint when your authorization server supports token revocation.
* Accept the RFC 8707 `resource` parameter in both authorization and token requests. Bind the issued token to the canonical URI of the MCP server.
* Return `401 Unauthorized` when a token is missing, expired, invalid, or issued for a different audience. Return `403 Forbidden` when the token is valid but lacks the required scope or entitlement.

For an unauthenticated request, point the client to that metadata and name the required scope:

```http
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://benefits.example.com/.well-known/oauth-protected-resource", scope="benefits.read"
```

For a valid token that lacks a required scope, return the scopes needed for that request:

```http
HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer error="insufficient_scope", scope="benefits.read", resource_metadata="https://benefits.example.com/.well-known/oauth-protected-resource"
```

### Use DCR for automatic client setup

When an administrator adds an MCP server URL, MCP Workspace can discover your metadata and register the OAuth client during setup with DCR. The authorization server exposes a `registration_endpoint` and accepts the client metadata that MCP Workspace sends.

Test the DCR path with your authorization server's registration policy. It must allow the redirect URI, `authorization_code` grant, PKCE, requested scopes, application type, and client-authentication method that your policy requires.

```mermaid
flowchart TB
    subgraph DCR[With DCR]
        W1[MCP Workspace] -->|register OAuth client| A1[Authorization server]
        A1 -->|client metadata| W1
        W1 -->|start employee consent| U1[Employee]
    end

    subgraph Manual[Without DCR]
        A2[Developer or administrator] -->|configure base URL and authentication| C[HTTP connector]
        C -->|connect the MCP server| W2[MCP Workspace]
    end
```

| Setup concern        | With DCR                                                                                  | Without DCR                                                                                               |
| -------------------- | ----------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| Initial connection   | The authorization server registers the client as part of the connection flow.             | A developer or administrator creates an HTTP connector with the MCP base URL and authentication settings. |
| Tenant rollout       | Registration policy can handle new customer environments consistently.                    | Each environment needs its own client configuration and review.                                           |
| Common support issue | Registration policy or redirect-URI validation fails in one visible flow.                 | Incorrect base URLs, token settings, redirect URIs, scopes, or copied configuration.                      |
| Developer experience | The administrator provides the endpoint; discovery and registration run as part of setup. | More setup documentation and a connector configuration before testing OAuth.                              |

DCR handles client setup only. It does not grant employee access, make a server trusted, or remove endpoint configuration and administrator governance in MCP Workspace.

If your authorization server cannot support DCR, configure it through an HTTP connector. The developer or administrator provides the MCP base URL and the authentication configuration required by the server. Document every required value, including token settings, redirect URI, scopes, and client details.

DCR registers an OAuth client. It does not authorize an employee and it does not replace access-token validation. Validate issuer, audience, expiry, scopes, the token's target resource, and any entitlement claims on every MCP request. Do not pass the MCP access token through to product APIs; obtain and validate a separate upstream credential when your server calls them.

If your customer signs in through SAML, that federation can remain behind your authorization server. The MCP-facing connection still uses OAuth tokens.

```mermaid
sequenceDiagram
    actor E as Employee
    participant W as MCP Workspace
    participant A as OAuth authorization server
    participant I as SAML identity provider
    participant S as Private MCP server

    E->>W: Use an MCP tool
    W->>A: OAuth authorization request with PKCE
    A->>I: Start federated SAML sign-in
    I-->>A: SAML response for the authenticated employee
    A-->>W: OAuth authorization code
    W->>A: Exchange code and PKCE verifier
    A-->>W: OAuth access token
    W->>S: MCP request with bearer token
```

The browser redirects are simplified here. The SAML response stays between the identity provider and authorization server. MCP Workspace receives an OAuth authorization code and sends an OAuth bearer token to the MCP server.

## From APIs to tools

Design tools around user tasks, not REST endpoints. A single tool can make several internal API calls before it returns an answer.

The [MCP tools specification](https://modelcontextprotocol.io/specification/2025-11-25/server/tools) defines the tool contract, including names, descriptions, input schemas, output schemas, and annotations.

```mermaid
flowchart LR
    Q[What medical coverage do I have?] --> T[get_my_benefits_coverage]
    T --> I[Employee identity and eligibility API]
    I --> P[Active plan API]
    P --> C[Coverage and policy APIs]
    C --> R[One concise coverage result]
```

A tool should cover one user task. It should not mirror every internal endpoint or try to answer every possible question.

| Tool shape                                                                   | Why it fails                                                                                | Better boundary                                                     |
| ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
| `get_policy_id`, `get_policy_details`, `get_deductible`, `get_plan_document` | The model must discover and pass intermediate IDs just to answer one employee question.     | Compose those calls inside `get_my_benefits_coverage(benefitType)`. |
| `answer_benefits_question(question: string)`                                 | The input and output are too open-ended to validate, route, test, or authorize predictably. | Keep a small set of named employee intents with typed inputs.       |
| `get_medical_coverage`, `get_dental_coverage`, `get_vision_coverage`         | The tools differ only by a stable category value.                                           | Use one `get_my_benefits_coverage(benefitType)` tool with an enum.  |

Let the model combine user-facing tools from the same server when the request or a follow-up makes each step clear. It can call `get_my_benefits_coverage` and then `get_my_policy_cost` without passing internal resource IDs between calls. This does not create automatic action chaining across MCP servers.

### Merge APIs or split tools?

Compose several API calls inside one tool when they:

* Always run in sequence to answer one employee question.
* Require intermediate identifiers that are not meaningful to the employee.
* Use the same authorization and side-effect boundary.
* Produce one coherent output shape.

Split out another tool when the employee intent, permission boundary, side effect, required input, or output shape changes. Coverage lookup, policy-cost lookup, and policy search should stay separate even when they share employee and plan data.

### A benefits example

| Employee question                           | APIs the server composes                                    | MCP tool                   | Typed input                 | Output                                              |
| ------------------------------------------- | ----------------------------------------------------------- | -------------------------- | --------------------------- | --------------------------------------------------- |
| "What does my medical plan cover?"          | Identity, eligibility, active plan, coverage, policy source | `get_my_benefits_coverage` | `benefitType`               | Plan, effective date, covered services, policy link |
| "What will this policy cost me?"            | Identity, eligibility, active plan, premiums                | `get_my_policy_cost`       | `benefitType`               | Employee cost, currency, frequency, effective date  |
| "What is the bereavement policy in France?" | Policy search, policy document, country rules               | `search_benefits_policy`   | `query`, optional `country` | Summary, applicability, source link                 |

Give every tool a unique name, a narrow scope, clear input constraints, and a stable output schema. Its description should say when to use it.

Use `readOnlyHint`, `destructiveHint`, and `idempotentHint` truthfully. These annotations describe tool behavior to the client. They do not replace authorization or business-rule enforcement in the server.

Tool descriptions drive routing. Tell the model when to call the tool, what it returns, and what it cannot do.

## Tools follow user access

At `tools/list`, return only the tools the current user can run. Resolve the user, tenant, and entitlements first, then check the same permission again when the tool runs.

```mermaid
flowchart LR
    T[Validated access token] --> P[Principal and tenant]
    P --> E[Current entitlements]
    E --> L[tools/list: authorized inventory]
    E --> C[tools/call: authorize again]
    C --> R[Authorized result or forbidden response]
```

| Authenticated user     | Example tools returned by `tools/list`                                                        | Data boundary                                                            |
| ---------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| Employee               | `get_my_benefits_coverage`, `get_my_policy_cost`, `search_benefits_policy`                    | Their own benefits and policies that apply to them                       |
| Benefits administrator | `get_employee_benefits_summary`, `list_plan_eligibility_exceptions`, `search_benefits_policy` | Employees and plans within the administrator's assigned population       |
| Policy author          | `search_benefits_policy`, `get_policy_version`, `preview_policy_applicability`                | Policy content within the author's permitted countries or business units |

For administrator actions, use a separate tool instead of adding an optional employee ID to a self-service tool. Keep `get_my_benefits_coverage` bound to the caller. Add `get_employee_benefits_summary(employeeIdentifier)` for authorized administrators, with its own scope, audit event, schema, output, and description.

Filtering the tool list is not enough. Clients can cache it, and roles can change. Check the user's current entitlement before every `tools/call` and return an authorization error when access is gone.

For each role, test the allowed path and the denied path:

* The intended tools appear in `tools/list` and execute successfully.
* Administrative tools never appear for an employee.
* A direct call to an admin tool with an employee token is rejected.
* Removing a role or scope causes the next tool call to be rejected, even when the client still has an older tool list.

### One tool, two SDKs

The helper names below are application code. `getAuthenticatedEmployee` reads the validated OAuth context; `getRequestId` reads the current request's correlation ID; and `eligibilityApi`, `plansApi`, `coverageApi`, and `policyApi` call product APIs. These are focused tool-registration examples, not complete server applications, so transport and authorization middleware are omitted.

```typescript title="benefits-server.ts" maxLines=0
import { McpServer } from "@modelcontextprotocol/server";
import * as z from "zod/v4";

const server = new McpServer({ name: "benefits", version: "1.0.0" });

const coverageSchema = z.object({
  benefitType: z.enum(["medical", "dental", "vision"]),
  planName: z.string(),
  effectiveDate: z.string(),
  coveredServices: z.array(z.string()),
  sourceUrl: z.string().url(),
});

const toolError = (code: string, message: string, requestId: string) => ({
  content: [
    {
      type: "text" as const,
      text: `${code}: ${message} Request ID: ${requestId}.`,
    },
  ],
  isError: true,
});

server.registerTool(
  "get_my_benefits_coverage",
  {
    title: "Get my benefits coverage",
    description:
      "Use when an authenticated employee asks what medical, dental, or vision " +
      "coverage applies to them. Uses the caller identity. Do not use for another " +
      "employee's coverage.",
    inputSchema: z.object({
      benefitType: z
        .enum(["medical", "dental", "vision"])
        .describe("The benefits category the employee asked about."),
    }),
    outputSchema: coverageSchema,
    annotations: { readOnlyHint: true },
  },
  async ({ benefitType }) => {
    const requestId = getRequestId();
    const employee = await getAuthenticatedEmployee();
    const eligibility = await eligibilityApi.getForEmployee({
      employeeId: employee.id,
      country: employee.country,
    });

    if (!eligibility.isEligible) {
      return toolError(
        "NO_ACTIVE_BENEFIT",
        "No active benefit is available.",
        requestId,
      );
    }

    const plan = await plansApi.getActivePlan({
      employeeId: employee.id,
      benefitType,
      country: employee.country,
    });

    if (!plan) {
      return toolError("BENEFIT_NOT_FOUND", "No active plan was found.", requestId);
    }

    const [coverage, policy] = await Promise.all([
      coverageApi.getCoverage({ planId: plan.id }),
      policyApi.getCurrentSource({ policyId: plan.policyId, country: employee.country }),
    ]);

    const result = coverageSchema.parse({
      benefitType,
      planName: plan.name,
      effectiveDate: plan.effectiveDate,
      coveredServices: coverage.services,
      sourceUrl: policy.url,
    });

    return {
      content: [
        {
          type: "text",
          text: `${result.planName} is active from ${result.effectiveDate}.`,
        },
      ],
      structuredContent: result,
    };
  },
);
```

```python title="benefits_server.py" maxLines=0
import asyncio
from typing import Annotated, Literal

from mcp.server.fastmcp import FastMCP
from mcp.types import CallToolResult, TextContent
from pydantic import BaseModel

mcp = FastMCP("Benefits", json_response=True)


class CoverageResult(BaseModel):
    benefitType: Literal["medical", "dental", "vision"]
    planName: str
    effectiveDate: str
    coveredServices: list[str]
    sourceUrl: str


@mcp.tool()
async def get_my_benefits_coverage(
    benefit_type: Literal["medical", "dental", "vision"],
) -> Annotated[CallToolResult, CoverageResult]:
    """Return the authenticated employee's coverage for one benefit category.

    Use when an employee asks about their medical, dental, or vision coverage.
    Do not use for another employee's coverage.

    Args:
        benefit_type: The benefits category to retrieve.
    """
    request_id = get_request_id()
    employee = await get_authenticated_employee()
    eligibility = await eligibility_api.get_for_employee(
        employee_id=employee.id,
        country=employee.country,
    )

    if not eligibility.is_eligible:
        return CallToolResult(
            content=[
                TextContent(
                    type="text",
                    text=(
                        "NO_ACTIVE_BENEFIT: No active benefit is available. "
                        f"Request ID: {request_id}."
                    ),
                )
            ],
            isError=True,
        )

    plan = await plans_api.get_active_plan(
        employee_id=employee.id,
        benefit_type=benefit_type,
        country=employee.country,
    )
    if plan is None:
        return CallToolResult(
            content=[
                TextContent(
                    type="text",
                    text=(
                        "BENEFIT_NOT_FOUND: No active plan was found. "
                        f"Request ID: {request_id}."
                    ),
                )
            ],
            isError=True,
        )

    coverage, policy = await asyncio.gather(
        coverage_api.get_coverage(plan_id=plan.id),
        policy_api.get_current_source(
            policy_id=plan.policy_id,
            country=employee.country,
        ),
    )
    result = CoverageResult(
        benefitType=benefit_type,
        planName=plan.name,
        effectiveDate=plan.effective_date.isoformat(),
        coveredServices=coverage.services,
        sourceUrl=policy.url,
    )
    return CallToolResult(
        content=[
            TextContent(
                type="text",
                text=f"{result.planName} is active from {result.effectiveDate}.",
            )
        ],
        structuredContent=result.model_dump(),
    )
```

## Responses and errors

Return the answer the employee needs. Do not return raw API responses, full policy documents, or hundreds of records for the model to filter.

For a benefits question, the server applies country, eligibility, plan-year, and entitlement rules. The model does not make those calls.

### Return a short answer and structured data

The text response gives the model enough context to answer the employee. `structuredContent` carries the same result in a predictable shape for clients that need it. It must match the tool's declared `outputSchema`.

```json title="Successful tool result"
{
  "content": [
    {
      "type": "text",
      "text": "Acme PPO is active from 2026-01-01 and covers preventive care and urgent care."
    }
  ],
  "structuredContent": {
    "benefitType": "medical",
    "planName": "Acme PPO",
    "effectiveDate": "2026-01-01",
    "coveredServices": ["preventive care", "urgent care"],
    "sourceUrl": "https://benefits.example.com/policies/acme-ppo"
  }
}
```

Do not make the model infer plan names, dates, or coverage rules from a raw upstream payload. Apply those rules in the server, then return only the fields needed for the employee's question.

### Return expected failures as tool errors

For an expected business failure, set `isError: true`, start the message with a stable error code, and include an opaque request ID for support. Keep the explanation safe to show to the employee.

```json title="Expected tool error"
{
  "content": [
    {
      "type": "text",
      "text": "NO_ACTIVE_BENEFIT: No active medical plan was found. Request ID: req_01JZ8Y7QW2."
    }
  ],
  "isError": true
}
```

Keep errors in `content` unless you deliberately define and test a structured error contract. Do not reuse success-shaped `structuredContent` for a failed result.

Use the response layer that matches the failure:

| Failure                                                   | Response                                                                                                                                           |
| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| Missing, expired, invalid, or wrong-audience access token | HTTP `401 Unauthorized` with `WWW-Authenticate`                                                                                                    |
| Valid access token without the required OAuth scope       | HTTP `403 Forbidden` with a `WWW-Authenticate` challenge that includes `error="insufficient_scope"`, the required `scope`, and `resource_metadata` |
| Input does not match the tool's `inputSchema`             | Let the SDK return the protocol validation error; do not run the tool                                                                              |
| Expected business condition, such as no active plan       | Tool result with `isError: true` and a stable code such as `NO_ACTIVE_BENEFIT`                                                                     |
| Temporary dependency failure                              | Tool result with `isError: true`, a code such as `UPSTREAM_UNAVAILABLE`, and safe retry guidance                                                   |
| Unexpected exception                                      | Tool result with `isError: true`, `INTERNAL_ERROR`, and an opaque request ID; keep the stack trace in server logs                                  |

### Reuse response helpers

Keep success and error formatting in shared helpers so every tool follows the same conventions.

```typescript title="response-helpers.ts" maxLines=0
type CoverageResult = {
  benefitType: "medical" | "dental" | "vision";
  planName: string;
  effectiveDate: string;
  coveredServices: string[];
  sourceUrl: string;
};

type ToolErrorCode =
  | "NO_ACTIVE_BENEFIT"
  | "UPSTREAM_UNAVAILABLE"
  | "INTERNAL_ERROR";

const coverageSuccess = (result: CoverageResult) => ({
  content: [
    {
      type: "text" as const,
      text: `${result.planName} is active from ${result.effectiveDate}.`,
    },
  ],
  structuredContent: result,
});

const toolError = (
  code: ToolErrorCode,
  message: string,
  requestId: string,
) => ({
  content: [
    {
      type: "text" as const,
      text: `${code}: ${message} Request ID: ${requestId}.`,
    },
  ],
  isError: true,
});
```

```python title="response_helpers.py" maxLines=0
from typing import Literal, TypedDict

from mcp.types import CallToolResult, TextContent


class CoverageResult(TypedDict):
    benefitType: Literal["medical", "dental", "vision"]
    planName: str
    effectiveDate: str
    coveredServices: list[str]
    sourceUrl: str


ToolErrorCode = Literal[
    "NO_ACTIVE_BENEFIT",
    "UPSTREAM_UNAVAILABLE",
    "INTERNAL_ERROR",
]


def coverage_success(result: CoverageResult) -> CallToolResult:
    return CallToolResult(
        content=[
            TextContent(
                type="text",
                text=f"{result['planName']} is active from {result['effectiveDate']}.",
            )
        ],
        structuredContent=result,
    )


def tool_error(
    code: ToolErrorCode,
    message: str,
    request_id: str,
) -> CallToolResult:
    return CallToolResult(
        content=[
            TextContent(
                type="text",
                text=f"{code}: {message} Request ID: {request_id}.",
            )
        ],
        isError=True,
    )
```

Never put tokens, employee identifiers, policy payloads, upstream response bodies, or stack traces in tool errors.

## Ownership and support

Connecting a private server does not transfer responsibility for that server to Moveworks.

| Owner                  | Responsibilities                                                                                                                                                                                                        |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Server owner           | Operate the endpoint and authorization server; validate tokens; enforce entitlements; maintain tool schemas and descriptions; handle upstream dependencies; and provide request IDs and server-side logs.               |
| Customer administrator | Configure the server or HTTP connector in MCP Workspace; review the exposed tools; enable the server; set its audience in Launch Configuration; and provide conversation evidence when escalating.                      |
| Moveworks              | Operate MCP Workspace and the MCP client; diagnose product-side connection, discovery, routing, and execution behavior; and document current product constraints. Moveworks does not review or certify private servers. |

For product-side failures, use [Limitations and Troubleshooting](/agent-studio/mcp-workspace/limitations-and-troubleshooting) to identify the failed stage and collect the required evidence.

## Test before connecting

Use the [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) before you connect the server to MCP Workspace. It lets you inspect the transport, tool inventory, schemas, results, and errors directly.

Test at least:

* Connection over Streamable HTTP.
* OAuth discovery, authorization, expiry, refresh, and invalid-token behavior.
* Your chosen setup path: DCR client metadata, redirect-URI validation, and registration-policy failures; or the manual HTTP connector configuration when the server does not support DCR.
* Every tool's schema, description, success result, and failure result.
* Missing inputs, invalid enum values, and permission-denied responses.
* Different countries, plans, eligibility states, and plan years.
* Rate limiting, upstream failures, concurrent requests, and log correlation.