Skip to content

feat: introduce auth and server-state protocols for hosted deployment#59

Merged
howbazaar merged 7 commits into
mainfrom
thumper/feat/auth-protocol
May 27, 2026
Merged

feat: introduce auth and server-state protocols for hosted deployment#59
howbazaar merged 7 commits into
mainfrom
thumper/feat/auth-protocol

Conversation

@howbazaar
Copy link
Copy Markdown
Contributor

what

Introduce two protocols that allow the MCP server to be parameterized for a hosted multi-user deployment:

AuthProvider protocol (server.py):

  • Defines get_credentials(ctx) -> StackletCredentials
  • LocalAuthProvider (auth.py) implements it for local stdio use, loading credentials from ~/.stacklet/ files or env vars (cached per process lifetime)
  • StackletCredentials.get() now delegates to the auth provider stored on server state, rather than calling server_cached directly

ServerStateProtocol (lifespan.py):

  • Extends the server state interface with auth_provider, ensure_cached, and ensure_cached_async
  • ServerState implements the protocol with the existing dict-based process-lifetime cache, now taking auth_provider at construction time
  • make_lifespan(auth_provider, state_factory=None) replaces the bare lifespan context manager; a hosted deployment can pass a custom state_factory to create per-request or per-user state
  • make_server(auth_provider=None, state_factory=None) wires everything together, defaulting to local behavior

Both client get() methods (PlatformClient, DocsClient) now type their server_state parameter as ServerStateProtocol rather than the concrete ServerState.

why

A hosted MCP deployment serves multiple users over HTTP/SSE, so credentials must come from per-request forwarded auth headers rather than a shared local config file. Parameterizing both auth and server state behind protocols lets the hosted deployment provide its own implementations without touching the tool or client code.

testing

All 215 existing tests pass.

docs

No documentation changes needed.

howbazaar and others added 3 commits May 19, 2026 23:10
### what

Move the GraphQL schema and docs index out of per-user client objects
into the server-level `ServerState` cache:

- Add `ensure_cached_async` to `ServerState` for objects that require
  async construction
- `PlatformClient.get_schema()` delegates to `server_state.ensure_cached_async`
  with a new `_fetch_schema` helper; removes the per-instance `_schema_cache`
- `DocsClient.get_index()` delegates to `server_state.ensure_cached_async`
  with a new `_fetch_index` helper; removes the per-instance `_index` list
- Both clients now receive the `ServerState` at construction time via
  their `get()` factory methods

### why

Per-user client objects are constructed fresh for each user in a hosted
deployment, so any cache stored on them is also per-user. The GraphQL
schema and docs index are tenant-wide resources that should be fetched
once and shared, not re-fetched for every user.

### testing

All 215 existing tests pass.

### docs

No documentation changes needed.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
- `ensure_cached_async`: fix race condition under concurrent async callers
  by using per-key `asyncio.Lock` with a double-check pattern; multiple
  coroutines entering before any result is stored now wait on the lock
  rather than each issuing a duplicate network request
- `PlatformClient.get`, `DocsClient.get`: remove redundant
  `assert ctx.request_context is not None` inside `construct()`; the
  outer `server_cached` call already asserts this with a descriptive
  message, so the inner assert was dead code. Use `# type: ignore[union-attr]`
  to satisfy mypy instead.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Introduce two protocols that allow the MCP server to be parameterized
for a hosted multi-user deployment:

**AuthProvider protocol** (`server.py`):
- Defines `get_credentials(ctx) -> StackletCredentials`
- `LocalAuthProvider` (`auth.py`) implements it for local stdio use,
  loading credentials from `~/.stacklet/` files or env vars (cached
  per process lifetime)
- `StackletCredentials.get()` now delegates to the auth provider stored
  on server state, rather than calling `server_cached` directly

**ServerStateProtocol** (`lifespan.py`):
- Extends the server state interface with `auth_provider`, `ensure_cached`,
  and `ensure_cached_async`
- `ServerState` implements the protocol with the existing dict-based
  process-lifetime cache, now taking `auth_provider` at construction time
- `make_lifespan(auth_provider, state_factory=None)` replaces the bare
  `lifespan` context manager; a hosted deployment can pass a custom
  `state_factory` to create per-request or per-user state
- `make_server(auth_provider=None, state_factory=None)` wires everything
  together, defaulting to local behavior

Both client `get()` methods (`PlatformClient`, `DocsClient`) now type
their `server_state` parameter as `ServerStateProtocol` rather than the
concrete `ServerState`.

A hosted MCP deployment serves multiple users over HTTP/SSE, so
credentials must come from per-request forwarded auth headers rather
than a shared local config file. Parameterizing both auth and server
state behind protocols lets the hosted deployment provide its own
implementations without touching the tool or client code.

All 215 existing tests pass.

No documentation changes needed.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
@howbazaar howbazaar requested a review from a team as a code owner May 21, 2026 03:18
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 804db61732

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread stacklet/mcp/lifespan.py Outdated
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 21, 2026

Greptile Summary

This PR introduces two extension points — AuthProvider and ServerStateProtocol — that allow the MCP server to be deployed in a hosted, multi-user HTTP/SSE context where credentials must come from per-request headers rather than a shared config file. The change is purely additive to the local stdio path: LocalAuthProvider preserves the existing behaviour, and make_server() defaults to it.

  • Auth delegation: StackletCredentials.get() now calls state.auth_provider.get_credentials(ctx) instead of directly reading from disk/env; LocalAuthProvider reimplements the original behaviour with process-lifetime caching.
  • Lifespan parameterisation: lifespan is replaced by make_lifespan(auth_provider, state_factory=None), which accepts a custom state factory so hosted deployments can supply per-request state; ServerState gains aclose() to close cached resources (including the new shared httpx.AsyncHTTPTransport) at shutdown.
  • Client changes: All three HTTP clients (PlatformClient, DocsClient, AssetDBClient) now create a fresh httpx.AsyncClient wrapping a cached httpx.AsyncHTTPTransport on every .get() call rather than caching the full client in server state.

Confidence Score: 5/5

Safe to merge — the auth and state protocol changes are well-scoped, local behaviour is fully preserved by the defaults, and all 215 tests pass.

The refactor correctly threads auth_provider through lifespan and delegates credential resolution without changing observable behaviour for the local stdio path. The new aclose() logic is a net improvement over the previous bare yield with no cleanup.

No files require special attention; the one comment left is a non-blocking resource-management observation about unclosed httpx.AsyncClient wrappers.

Important Files Changed

Filename Overview
stacklet/mcp/lifespan.py Core change: ServerState gains aclose() and auth_provider; lifespan replaced by make_lifespan(); ServerStateProtocol introduced. Logic is sound and ensure_cached_async double-checked lock is correctly preserved.
stacklet/mcp/server.py Adds AuthProvider protocol and wires make_server() to accept auth_provider / state_factory; defaults to LocalAuthProvider and ServerState. Clean and minimal.
stacklet/mcp/auth.py New file implementing LocalAuthProvider; delegates to server_cached for process-lifetime credential caching, preserving the original behaviour.
stacklet/mcp/stacklet_auth.py StackletCredentials.get() now delegates through state.auth_provider; the assertion for request context is a clean guard.
stacklet/mcp/platform/graphql.py Client parameter widened to ServerStateProtocol; transport now cached in state. get_schema() return type loses static guarantee (return is Any from ensure_cached_async) — flagged in previous review, not repeated here.
stacklet/mcp/docs/client.py Parameter widened to ServerStateProtocol; transport cached; client no longer cached in state (created fresh per .get() call). Pattern consistent with platform and assetdb clients.
stacklet/mcp/assetdb/redash.py server_state added to constructor for transport caching; client no longer cached in state. Consistent with the other client refactors.
tests/test_lifespan.py New tests covering ServerState.aclose() and end-to-end auth-provider delegation; good coverage of the new protocol boundaries.
tests/conftest.py Monkeypatch target updated from stacklet_auth.load_stacklet_auth to auth.load_stacklet_auth — correct, since auth.py now imports and calls load_stacklet_auth.

Sequence Diagram

sequenceDiagram
    participant Tool as MCP Tool
    participant SC as StackletCredentials.get()
    participant State as ServerState (lifespan context)
    participant AP as AuthProvider
    participant LC as LocalAuthProvider
    participant FS as load_stacklet_auth()

    Tool->>SC: get(ctx)
    SC->>State: ctx.request_context.lifespan_context
    SC->>AP: state.auth_provider.get_credentials(ctx)
    AP->>LC: (LocalAuthProvider instance)
    LC->>State: server_cached(ctx, "STACKLET_CREDS", load_stacklet_auth)
    alt first call (not cached)
        State->>FS: load_stacklet_auth()
        FS-->>State: StackletCredentials
        State-->>LC: cached credentials
    else subsequent calls
        State-->>LC: cached credentials
    end
    LC-->>SC: StackletCredentials
    SC-->>Tool: StackletCredentials

    Note over Tool,FS: PlatformClient / DocsClient / AssetDBClient each call .get() per tool invocation
    Tool->>State: ensure_cached("HTTP_TRANSPORT", AsyncHTTPTransport)
    State-->>Tool: shared transport (cached)
    Tool->>Tool: "httpx.AsyncClient(transport=shared_transport, ...)"

    Note over State: At shutdown: state.aclose() -> transport.aclose()
Loading

Fix All in Claude Code

Reviews (4): Last reviewed commit: "Fix the lint." | Re-trigger Greptile

Comment thread stacklet/mcp/server.py
Base automatically changed from thumper/feat/cache-schema-and-docs to main May 22, 2026 03:30
Comment thread stacklet/mcp/docs/client.py
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6f7fa40cd1

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread stacklet/mcp/platform/graphql.py
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 26, 2026

Want your agent to iterate on Greptile's feedback? Try greploops.

Comment thread tests/conftest.py
)

monkeypatch.setattr("stacklet.mcp.stacklet_auth.load_stacklet_auth", lambda: fake_credentials)
monkeypatch.setattr("stacklet.mcp.auth.load_stacklet_auth", lambda: fake_credentials)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nothing tests the new credential path — every test builds the server with defaults, so StackletCredentials.get routing through auth_provider, and ServerState.aclose() closing the transport, are both unexercised. A regression that broke the provider delegation or skipped cleanup on exit would stay green across the suite. Worth one test that passes a stub provider and asserts it's used, plus one that asserts aclose() closes the cached transport.

Comment thread stacklet/mcp/docs/client.py Outdated
async def get_index(self) -> list[DocFile]:
"""Fetch documents index, using the server-level cache."""
return await self.server_state.ensure_cached_async("DOCS_INDEX", self._fetch_index)
return cast(
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

small one: the protocol types ensure_cached/ensure_cached_async as -> Any, dropping the ServerCached TypeVar the concrete ServerState keeps — that's why get_index needs the cast(list[DocFile], …) here. Giving the protocol methods the TypeVar lets the cast go away and also clears a Returning Any on get_schema in graphql.py. (Only uv run mypy surfaces it — the pre-commit hook doesn't install graphql-core/httpx.)

@howbazaar howbazaar merged commit 37b38c1 into main May 27, 2026
6 checks passed
@howbazaar howbazaar deleted the thumper/feat/auth-protocol branch May 27, 2026 04:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants