feat(mcp_gateway): add RFC 7662 introspection for gateway session tokens (#38726)

* feat(mcp_gateway): add RFC 7662 introspection for gateway session tokens

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(mcp_gateway): omit Bearer token_type for refresh introspection and allow mcp-scoped keys

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(mcp_gateway): cover introspection of RS256-signed session tokens

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(mcp_gateway): load the discoverable router on a cold /introspect request

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* chore(openapi): regenerate lazy snapshot and schema.d.ts for /introspect

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: yassin <yassin@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
devin-ai-integration[bot] 2026-08-29 12:56:04 -07:00 committed by GitHub
parent 9ed7de6c02
commit f0340fef16
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 475 additions and 7 deletions

View file

@ -9,7 +9,7 @@ from typing import TYPE_CHECKING, Any, Final, Literal, Optional
from urllib.parse import parse_qsl, urlencode, urlparse, urlunparse
import httpx
from fastapi import APIRouter, Form, HTTPException, Request
from fastapi import APIRouter, Depends, Form, HTTPException, Request
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse, Response
from pydantic import BaseModel, ConfigDict, Field, SecretStr, ValidationError
@ -46,6 +46,7 @@ from litellm.proxy._experimental.mcp_server.gateway_dcr_flow import (
aggregate_authorize,
aggregate_token,
complete_connect_flow,
introspect_gateway_token,
is_gateway_dcr_client_id,
is_proxy_api_resource,
native_client_auth_contract,
@ -67,6 +68,7 @@ from litellm.proxy._experimental.mcp_server.proxy_api_credentials import (
mint_proxy_credential,
)
from litellm.proxy.auth.ip_address_utils import IPAddressUtils
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
from litellm.proxy.common_utils.encrypt_decrypt_utils import (
decrypt_value_helper,
encrypt_value_helper,
@ -1951,6 +1953,26 @@ async def revoke_endpoint(request: Request, token: str = Form(...), client_id: s
return await revoke_refresh_token(token=token, client_id=client_id, master_key=master_key, cache=user_api_key_cache)
@router.post("/introspect", dependencies=[Depends(user_api_key_auth)])
async def introspect_endpoint(token: str = Form(...)) -> Response:
"""RFC 7662 introspection for gateway-issued session tokens (``llm_session_`` /
``llm_srefresh_``), so an external gateway can validate them without the signing
secret. The caller authenticates with a LiteLLM virtual key (section 2.1, enforced by
the route dependency); any token the gateway cannot vouch for answers
``{"active": false}`` with no further detail."""
from litellm.proxy.proxy_server import ( # noqa: PLC0415 # circular import at module load
master_key,
user_api_key_cache,
)
return await introspect_gateway_token(
token=token,
master_key=master_key,
reload_user=_reload_active_user_by_id,
cache=user_api_key_cache,
)
@router.get("/.well-known/litellm-cli-auth")
async def native_client_auth_discovery(request: Request) -> JSONResponse:
"""The versioned contract a native client (``lite login --pkce``, or a CLI in any other
@ -2456,6 +2478,7 @@ def _build_aggregate_authorization_server_response(request: Request) -> dict:
"issuer": f"{request_base_url}/mcp",
"authorization_endpoint": f"{request_base_url}/authorize",
"token_endpoint": f"{request_base_url}/token",
"introspection_endpoint": f"{request_base_url}/introspect",
"registration_endpoint": f"{request_base_url}/register",
"response_types_supported": ["code"],
"scopes_supported": [],

View file

@ -70,13 +70,19 @@ from litellm.proxy._experimental.mcp_server.outbound_credentials.session_credent
open_session_refresh_bearer,
)
from litellm.proxy._experimental.mcp_server.outbound_credentials.session_token import (
SESSION_ISSUER,
SESSION_REFRESH_TTL_SECONDS,
MintedSessionToken,
OpenedSessionToken,
SessionAudience,
SessionPrincipal,
SessionSigningKeys,
is_session_refresh_token,
is_session_token,
mint_session_refresh_token,
mint_session_token,
open_session_refresh_token,
open_session_token,
)
from litellm.proxy.common_utils.encrypt_decrypt_utils import (
decrypt_value_helper,
@ -885,6 +891,23 @@ class _SingleUseGuard:
count = await self._cache.async_increment_cache(key, 1, ttl=ttl_seconds, local_only=True)
return "first" if count == 1 else "replayed"
async def peek(self, key: str) -> Literal["unclaimed", "claimed", "unavailable"]:
"""Read-only view of a single-use marker, resolved against the same shared authority as
:meth:`claim` so introspection observes exactly the record redemption and revocation wrote.
A backend fault is ``"unavailable"`` (fail closed) rather than a guess either way."""
from litellm.proxy.proxy_server import redis_usage_cache # noqa: PLC0415 # circular import at module load
redis_cache: Final = redis_usage_cache or getattr(self._cache, "redis_cache", None)
if redis_cache is not None:
try:
value = await redis_cache.async_get_cache(key)
except Exception as e: # noqa: BLE001 # ANY Redis fault fails the read closed
verbose_logger.warning("mcp gateway single-use peek: shared cache backend unavailable: %s", e)
return "unavailable"
return "unclaimed" if value is None else "claimed"
local: Final = await self._cache.async_get_cache(key, local_only=True)
return "unclaimed" if local is None else "claimed"
def _session_token_pair(principal: SessionPrincipal, keys: SessionSigningKeys, now: datetime) -> Response:
access: Final = mint_session_token(principal, keys, now)
@ -1199,3 +1222,83 @@ async def revoke_refresh_token(token: str, client_id: str, master_key: str | Non
if burned == "unavailable":
return _oauth_error(503, "temporarily_unavailable", _CLAIM_UNAVAILABLE_DESCRIPTION)
return Response(content="{}", media_type="application/json", headers=TOKEN_NO_CACHE_HEADERS)
def _inactive_introspection_response() -> Response:
"""RFC 7662 section 2.2: any token the gateway cannot vouch for, whatever the reason
(wrong family, bad signature, expired, revoked, or a deactivated user), answers 200
with ``active: false`` and nothing else, so introspection is not a token oracle."""
return JSONResponse(status_code=200, content={"active": False}, headers=TOKEN_NO_CACHE_HEADERS)
def _active_introspection_response(opened: OpenedSessionToken) -> Response:
principal: Final = opened.principal
optional_claims: Final = {
key: value
for key, value in (
("token_type", "Bearer" if opened.kind == "session" else None),
("team_id", principal.team_id),
("resource_server_id", principal.resource_server_id),
("audience", principal.audience),
)
if value is not None
}
return JSONResponse(
status_code=200,
content={
"active": True,
"iss": SESSION_ISSUER,
"sub": principal.user_id,
"client_id": principal.client_id,
"jti": opened.jti,
"iat": opened.iat,
"exp": opened.exp,
"kind": opened.kind,
**optional_claims,
},
headers=TOKEN_NO_CACHE_HEADERS,
)
async def introspect_gateway_token(
token: str,
master_key: str | None,
reload_user: ReloadUser,
cache: DualCache,
) -> Response:
"""RFC 7662 introspection for the gateway's session tokens, so an external gateway
(Kong, an API management layer) can validate a LiteLLM-issued MCP session credential
without holding the signing secret. The caller is already authenticated by the route
(section 2.1). Active means everything admission itself would require: valid signature
under the configured session signing keys, unexpired, not a revoked or rotated refresh
token, and a litellm user that is still live, so a deactivated user's outstanding
tokens introspect as inactive immediately. A shared-backend or DB outage answers 503
rather than guessing in either direction."""
if master_key is None:
verbose_logger.error("mcp_gateway_dcr introspect rejected: no master_key configured")
return _oauth_error(500, "server_error", "the gateway has no master key configured")
keys: Final = active_session_signing_keys(master_key)
if isinstance(keys, SessionSigningConfigError):
verbose_logger.error("mcp_gateway_dcr introspect rejected: %s", keys.detail)
return _oauth_error(500, "server_error", keys.detail)
now: Final = datetime.now(timezone.utc)
if is_session_token(token):
opened = open_session_token(token, keys, now)
elif is_session_refresh_token(token):
opened = open_session_refresh_token(token, keys, now)
else:
return _inactive_introspection_response()
if not isinstance(opened, OpenedSessionToken):
return _inactive_introspection_response()
if opened.kind == "session_refresh":
peeked: Final = await _SingleUseGuard(cache).peek(f"{_USED_REFRESH_CACHE_PREFIX}{opened.jti}")
if peeked == "unavailable":
return _oauth_error(503, "temporarily_unavailable", _CLAIM_UNAVAILABLE_DESCRIPTION)
if peeked == "claimed":
return _inactive_introspection_response()
failure: Final = await reload_user(opened.principal.user_id)
if failure == "unavailable":
return _oauth_error(503, "temporarily_unavailable", "the gateway database is unavailable; retry")
if failure is not None:
return _inactive_introspection_response()
return _active_introspection_response(opened)

View file

@ -221,12 +221,17 @@ class MintedSessionToken(BaseModel):
class OpenedSessionToken(BaseModel):
"""A validated session token of either kind: the principal it was minted for, plus the
``jti`` so the token endpoint can enforce single-use rotation on a refresh token."""
"""A validated session token of either kind: the principal it was minted for, the
``jti`` so the token endpoint can enforce single-use rotation on a refresh token, and
the signed ``kind``/``iat``/``exp`` so an introspection response can report the
token's metadata without re-decoding."""
model_config = ConfigDict(frozen=True)
principal: SessionPrincipal
jti: str
kind: SessionTokenKind
iat: int
exp: int
class SessionTokenTooLarge(BaseModel):
@ -458,6 +463,9 @@ def _open(
team_id=claims.team_id,
),
jti=claims.jti,
kind=claims.kind,
iat=claims.iat,
exp=claims.exp,
)

View file

@ -161,6 +161,7 @@ LAZY_FEATURES: Final[tuple[LazyFeature, ...]] = (
"/callback",
"/register",
"/revoke",
"/introspect",
),
# Catches the /{mcp_server_name}/authorize|token|register variants.
path_suffixes=("/authorize", "/token", "/register"),

View file

@ -16555,6 +16555,19 @@
"title": "Body_authorize_complete_authorize_complete_post",
"type": "object"
},
"Body_introspect_endpoint_introspect_post": {
"properties": {
"token": {
"title": "Token",
"type": "string"
}
},
"required": [
"token"
],
"title": "Body_introspect_endpoint_introspect_post",
"type": "object"
},
"Body_revoke_endpoint_revoke_post": {
"properties": {
"client_id": {
@ -19134,6 +19147,51 @@
]
}
},
"/introspect": {
"post": {
"description": "RFC 7662 introspection for gateway-issued session tokens (``llm_session_`` /\n``llm_srefresh_``), so an external gateway can validate them without the signing\nsecret. The caller authenticates with a LiteLLM virtual key (section 2.1, enforced by\nthe route dependency); any token the gateway cannot vouch for answers\n``{\"active\": false}`` with no further detail.",
"operationId": "introspect_endpoint_introspect_post",
"requestBody": {
"content": {
"application/x-www-form-urlencoded": {
"schema": {
"$ref": "#/components/schemas/Body_introspect_endpoint_introspect_post"
}
}
},
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {}
}
},
"description": "Successful Response"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
"description": "Validation Error"
}
},
"security": [
{
"APIKeyHeader": []
}
],
"summary": "Introspect Endpoint",
"tags": [
"mcp_discoverable"
]
}
},
"/register": {
"post": {
"operationId": "register_client_register_post",

View file

@ -504,6 +504,7 @@ class LiteLLMRoutes(enum.Enum):
"/mcp-rest/tools/list",
"/mcp-rest/tools/call",
"/v1/mcp/tools",
"/introspect",
]
# MCP server CRUD routes — control-plane. Gated by DISABLE_ADMIN_ENDPOINTS.

View file

@ -10290,3 +10290,82 @@ def test_native_client_authorize_without_the_proxy_resource_keeps_the_mcp_flow(m
assert 'name="decision"' not in response.text
assert "team-b" not in response.text
assert minted == []
def test_introspect_route_requires_virtual_key_auth_and_is_advertised():
"""RFC 7662 section 2.1: introspection must not be anonymous. Pins the route-level
user_api_key_auth dependency (structure, so removing it fails here without a proxy),
and that the aggregate AS metadata advertises the endpoint for discovery."""
from fastapi import FastAPI
from fastapi.routing import APIRoute
from fastapi.testclient import TestClient
from litellm.proxy._experimental.mcp_server.discoverable_endpoints import router
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
route = next(r for r in router.routes if isinstance(r, APIRoute) and r.path == "/introspect")
assert route.methods == {"POST"}
assert any(dependency.call is user_api_key_auth for dependency in route.dependant.dependencies)
from litellm.proxy._types import LiteLLMRoutes
assert "/introspect" in LiteLLMRoutes.mcp_routes.value
from litellm.proxy._lazy_features import LAZY_FEATURES
discoverable = next(feature for feature in LAZY_FEATURES if feature.name == "mcp_discoverable")
assert "/introspect" in discoverable.path_prefixes
app = FastAPI()
app.include_router(router)
client = TestClient(app)
asm = client.get("/.well-known/oauth-authorization-server/mcp")
assert asm.json()["introspection_endpoint"] == "http://testserver/introspect"
def test_introspect_route_answers_for_authenticated_caller(monkeypatch):
"""End-to-end over the real route with the auth dependency satisfied: a garbage token
is active false, a freshly minted session access token is active true with its claims."""
from datetime import datetime, timezone
from fastapi import FastAPI
from fastapi.testclient import TestClient
from litellm.proxy._experimental.mcp_server.discoverable_endpoints import router
from litellm.proxy._experimental.mcp_server.outbound_credentials.session_credentials import (
session_keys_from_master_key,
)
from litellm.proxy._experimental.mcp_server.outbound_credentials.session_token import (
SessionPrincipal,
mint_session_token,
)
from litellm.proxy._types import UserAPIKeyAuth
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
introspect_master_key = "sk-introspect-route-test"
monkeypatch.setattr("litellm.proxy.proxy_server.master_key", introspect_master_key, raising=False)
async def fake_reload(user_id: str):
return None
monkeypatch.setattr(
"litellm.proxy._experimental.mcp_server.discoverable_endpoints._reload_active_user_by_id", fake_reload
)
app = FastAPI()
app.include_router(router)
app.dependency_overrides[user_api_key_auth] = lambda: UserAPIKeyAuth()
client = TestClient(app)
garbage = client.post("/introspect", data={"token": "llm_session_garbage"})
assert garbage.status_code == 200
assert garbage.json() == {"active": False}
minted = mint_session_token(
SessionPrincipal(user_id="u1", client_id="llm_dcrc_client"),
session_keys_from_master_key(introspect_master_key),
datetime.now(timezone.utc),
)
active = client.post("/introspect", data={"token": minted.token.get_secret_value()})
assert active.status_code == 200
assert active.json()["active"] is True
assert active.json()["sub"] == "u1"

View file

@ -26,6 +26,7 @@ from litellm.proxy._experimental.mcp_server.gateway_dcr_flow import (
aggregate_authorize,
aggregate_token,
complete_connect_flow,
introspect_gateway_token,
is_gateway_dcr_client_id,
is_proxy_api_resource,
native_client_auth_contract,
@ -41,7 +42,13 @@ from litellm.proxy._experimental.mcp_server.outbound_credentials.session_credent
resolve_session_bearer,
session_keys_from_master_key,
)
from litellm.proxy._experimental.mcp_server.outbound_credentials.session_token import SESSION_REFRESH_PREFIX
from litellm.proxy._experimental.mcp_server.outbound_credentials.session_token import (
SESSION_ISSUER,
SESSION_REFRESH_PREFIX,
SessionPrincipal,
mint_session_refresh_token,
mint_session_token,
)
MASTER_KEY = "sk-gateway-dcr-flow-tests"
REDIRECT_URI = "https://claude.ai/api/mcp/auth_callback"
@ -1598,17 +1605,24 @@ async def test_refresh_answers_503_without_burning_the_token_while_redis_is_down
)
redis_down = await _refresh_native(
payload["refresh_token"], client_id, _Minter(), _redis_that(AsyncMock(side_effect=ConnectionError("redis down")))
payload["refresh_token"],
client_id,
_Minter(),
_redis_that(AsyncMock(side_effect=ConnectionError("redis down"))),
)
assert redis_down.status_code == 503
assert json.loads(redis_down.body)["error"] == "temporarily_unavailable"
assert "refresh_token" not in json.loads(redis_down.body)
redis_back = await _refresh_native(payload["refresh_token"], client_id, _Minter(), _redis_that(AsyncMock(return_value=1)))
redis_back = await _refresh_native(
payload["refresh_token"], client_id, _Minter(), _redis_that(AsyncMock(return_value=1))
)
assert redis_back.status_code == 200
assert json.loads(redis_back.body)["refresh_token"] != payload["refresh_token"]
replayed = await _refresh_native(payload["refresh_token"], client_id, _Minter(), _redis_that(AsyncMock(return_value=2)))
replayed = await _refresh_native(
payload["refresh_token"], client_id, _Minter(), _redis_that(AsyncMock(return_value=2))
)
assert replayed.status_code == 400
assert json.loads(replayed.body)["error"] == "invalid_grant"
@ -1674,3 +1688,122 @@ def test_native_client_auth_contract_points_every_endpoint_at_this_proxy():
)
def test_is_proxy_api_resource_matches_only_this_proxy(resource, expected):
assert is_proxy_api_resource(_request(), resource) is expected
def _introspection_fixtures():
keys = session_keys_from_master_key(MASTER_KEY)
now = datetime.now(timezone.utc)
principal = SessionPrincipal(user_id="u1", client_id="llm_dcrc_client", team_id="t1")
return keys, now, principal
async def _introspect(token, cache=None, reload_user=_reload_user_active, master_key=MASTER_KEY):
response = await introspect_gateway_token(
token=token, master_key=master_key, reload_user=reload_user, cache=cache or DualCache()
)
return response.status_code, json.loads(response.body)
@pytest.mark.asyncio
async def test_introspect_active_access_token_reports_rfc7662_claims():
keys, now, principal = _introspection_fixtures()
minted = mint_session_token(principal, keys, now)
status, body = await _introspect(minted.token.get_secret_value())
assert status == 200
assert body["active"] is True
assert body["token_type"] == "Bearer"
assert body["iss"] == SESSION_ISSUER
assert body["sub"] == "u1"
assert body["client_id"] == "llm_dcrc_client"
assert body["kind"] == "session"
assert body["team_id"] == "t1"
assert body["exp"] - body["iat"] == 3600
assert body["jti"]
@pytest.mark.asyncio
async def test_introspect_invalid_tokens_answer_active_false():
keys, now, principal = _introspection_fixtures()
wrong_key = mint_session_token(principal, session_keys_from_master_key("sk-a-rotated-master-key"), now)
expired = mint_session_token(principal, keys, now - timedelta(seconds=7200))
for candidate in (
"sk-not-a-session-token",
"llm_session_malformed",
wrong_key.token.get_secret_value(),
expired.token.get_secret_value(),
):
status, body = await _introspect(candidate)
assert (status, body) == (200, {"active": False})
@pytest.mark.asyncio
async def test_introspect_refresh_token_goes_inactive_once_rotated():
keys, now, _ = _introspection_fixtures()
client_id = (await _register([REDIRECT_URI]))["client_id"]
minted = mint_session_refresh_token(SessionPrincipal(user_id="u1", client_id=client_id), keys, now)
cache = DualCache()
status, body = await _introspect(minted.token.get_secret_value(), cache=cache)
assert (status, body["active"], body["kind"]) == (200, True, "session_refresh")
assert "token_type" not in body
revoked = await revoke_refresh_token(
token=minted.token.get_secret_value(), client_id=client_id, master_key=MASTER_KEY, cache=cache
)
assert revoked.status_code == 200
status, body = await _introspect(minted.token.get_secret_value(), cache=cache)
assert (status, body) == (200, {"active": False})
@pytest.mark.asyncio
async def test_introspect_accepts_rs256_signed_tokens_under_configured_signing(monkeypatch):
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from pydantic import SecretStr
from litellm.proxy import proxy_server
from litellm.proxy._experimental.mcp_server.outbound_credentials.session_token import AsymmetricSessionKeys
private_pem = (
rsa.generate_private_key(public_exponent=65537, key_size=2048)
.private_bytes(
serialization.Encoding.PEM,
serialization.PrivateFormat.PKCS8,
serialization.NoEncryption(),
)
.decode()
)
monkeypatch.setitem(
proxy_server.general_settings,
"mcp_session_token_signing",
{"algorithm": "RS256", "kid": "k1", "private_key": private_pem},
)
_, now, principal = _introspection_fixtures()
rs_keys = AsymmetricSessionKeys(private_key_pem=SecretStr(private_pem), kid="k1")
minted = mint_session_token(principal, rs_keys, now)
status, body = await _introspect(minted.token.get_secret_value())
assert (status, body["active"], body["kind"]) == (200, True, "session")
hs_signed = mint_session_token(principal, session_keys_from_master_key(MASTER_KEY), now)
status, body = await _introspect(hs_signed.token.get_secret_value())
assert (status, body) == (200, {"active": False})
@pytest.mark.asyncio
async def test_introspect_fails_closed_on_dead_user_and_503s_on_outage():
keys, now, principal = _introspection_fixtures()
minted = mint_session_token(principal, keys, now)
async def _reload_user_gone(user_id: str):
return "unresolvable"
status, body = await _introspect(minted.token.get_secret_value(), reload_user=_reload_user_gone)
assert (status, body) == (200, {"active": False})
async def _reload_user_outage(user_id: str):
return "unavailable"
status, body = await _introspect(minted.token.get_secret_value(), reload_user=_reload_user_outage)
assert (status, body["error"]) == (503, "temporarily_unavailable")
status, body = await _introspect(minted.token.get_secret_value(), master_key=None)
assert (status, body["error"]) == (500, "server_error")

View file

@ -6974,6 +6974,30 @@ export interface paths {
patch?: never;
trace?: never;
};
"/introspect": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/**
* Introspect Endpoint
* @description RFC 7662 introspection for gateway-issued session tokens (``llm_session_`` /
* ``llm_srefresh_``), so an external gateway can validate them without the signing
* secret. The caller authenticates with a LiteLLM virtual key (section 2.1, enforced by
* the route dependency); any token the gateway cannot vouch for answers
* ``{"active": false}`` with no further detail.
*/
post: operations["introspect_endpoint_introspect_post"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/invitation/delete": {
parameters: {
query?: never;
@ -23474,6 +23498,11 @@ export interface components {
/** Mask[] */
"mask[]"?: string[] | null;
};
/** Body_introspect_endpoint_introspect_post */
Body_introspect_endpoint_introspect_post: {
/** Token */
token: string;
};
/** Body_revoke_endpoint_revoke_post */
Body_revoke_endpoint_revoke_post: {
/** Client Id */
@ -47651,6 +47680,39 @@ export interface operations {
};
};
};
introspect_endpoint_introspect_post: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/x-www-form-urlencoded": components["schemas"]["Body_introspect_endpoint_introspect_post"];
};
};
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": unknown;
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
invitation_delete_invitation_delete_post: {
parameters: {
query?: never;