Source code for kumiho.request_context

"""Per-request tenant/user context for hosted (multi-tenant) deployments.

The stdio MCP server is single-tenant by construction: one process, one user,
one set of credentials in the environment. A hosted deployment inverts that —
one process serves many tenants concurrently — so every piece of state that
used to be safely process-global (auth token, caches, session pointers) has to
become request-scoped instead.

This module is the seam. A hosting layer (``kumiho-plugins/cloud-mcp``) builds
a :class:`RequestContext` from the verified bearer token and enters
:func:`request_context` for the duration of the request; everything downstream
reads it through :func:`current_request`.

A ``contextvars.ContextVar`` is the right carrier rather than a thread-local:
the MCP server dispatches tool handlers with ``asyncio.to_thread``, which
copies the current context into the worker thread, so the value follows the
request across the async/sync boundary without being passed explicitly through
every call site.

Rules when :func:`current_request` returns a context (see the hosted-connector
plan, §2.1):

- never read ``~/.kumiho/*``, never mutate ``os.environ``, never use the
  machine-id discovery cache file;
- every per-process cache must be keyed by ``tenant_id``;
- ``kumiho.get_client()`` must resolve through :func:`kumiho.use_client`, and
  ``kumiho_memory`` must set ``redis_token_override_var`` from
  ``ctx.auth_token`` when it is unset.

:func:`hosted_mode` is the coarse, process-wide opt-in
(``KUMIHO_MCP_HOSTED=1``). It is deliberately separate from
:func:`current_request`: a hosted process must behave defensively (no env
mutation, no filesystem writes) even on code paths that run outside a request,
such as server construction.

Importing
---------

``RequestContext``, ``current_request`` and ``hosted_mode`` are re-exported
from the package root. The context manager is not, because a submodule and a
package attribute share one namespace: binding ``kumiho.request_context`` to
the *function* overwrites the module attribute, and
``import kumiho.request_context as rc`` — which resolves through
``getattr(kumiho, "request_context")`` rather than ``sys.modules`` — then hands
back the function. Use either spelling instead::

    from kumiho.request_context import request_context
    kumiho.use_request_context   # the same object, root-level, and symmetric
                                 # with kumiho.use_client
"""

from __future__ import annotations

import contextvars
from dataclasses import dataclass, field
from typing import Iterator, List, Optional
from contextlib import contextmanager


[docs] @dataclass(frozen=True) class RequestContext: """Identity and routing for a single hosted request. Frozen on purpose: a handler that mutated the context would change it for every other frame sharing the same ``contextvars`` copy, including worker threads already running. Replace it with :func:`request_context` instead. """ tenant_id: str # UUID from token claims user_id: str # firebase uid (OAuth) or "service:<token_id>" (API key) auth_token: str # the raw bearer/api-key JWT presented by the caller context: str = "claude" # memory context namespace (active-session pointer key) session_id: Optional[str] = None client_id: Optional[str] = None scopes: List[str] = field(default_factory=list) tenant_slug: Optional[str] = None region_code: Optional[str] = None token_id: Optional[str] = None # jti
_request_var: contextvars.ContextVar[Optional[RequestContext]] = contextvars.ContextVar( "kumiho_request", default=None )
[docs] def current_request() -> Optional[RequestContext]: """Return the context for the in-flight request, or ``None`` when local.""" return _request_var.get()
@contextmanager def request_context(ctx: RequestContext) -> Iterator[RequestContext]: """Bind *ctx* for the duration of the block. Resets to the previous value on exit — including on exception — so nested and concurrent requests never leak into each other. """ token = _request_var.set(ctx) try: yield ctx finally: _request_var.reset(token)
[docs] def hosted_mode() -> bool: """True when this process is running as a multi-tenant hosted server.""" import os return os.environ.get("KUMIHO_MCP_HOSTED", "").strip().lower() in ("1", "true", "yes")
__all__ = [ "RequestContext", "current_request", "request_context", "hosted_mode", ]