Adam Schembri’s personal chief of staff, built as the SCSP MCP connector in miniature: Claude connects to one remote MCP connector (Cloud Run, Google sign-in) that reads Adam’s own GitHub, Jira and Confluence, Calendar, Gmail and Okta. It proves the SCSP design on one person before SCSP depends on it. Read HANDOFF.md first (current state, Adam’s pending tasks, the plan for the next step), then README.md for the step table.
Hillspire/adam-cos-lab repo, deleted once the move is merged. Changes go
through PRs like everything else in this repo; the Claude reviewer and the “SCSP CoS Lab
Tests” workflow cover this folder.adam-cos-lab (schmidtentity.com org, same billing as Adam’s other
projects). Jira: ITPROJ-1421, under SCSP epic ITPROJ-1401. Comment on the ticket in
first person when a step lands.../HANDOFF.md, ../CLAUDE.md) and Confluence IPO →
3.M SCSP Initiative.../../gwen_grant_memo_agent/mcp_oauth.py.lab, imported relative to it
(pyproject.toml sets pythonpath = ["."])..readonly; no write method appears in lab/tools/google.py.frame() wraps every result in UNTRUSTED_*_DATA markers.history_content().gh auth token plus op run; deployed, Secret Manager
in adam-cos-lab only. Never gw-admin-assistant, where harry-devs can read every secret.lab/connector/, step 2)token_verifier; drop it and /mcp is silently open. A test walks every route, mounts
included: anything outside the listed sign-in and discovery endpoints must answer 401.iss in front of ours.
Claude’s hosted callback (claude.ai) waits for step 7: a sign-in started in someone
else’s claude.ai account and forwarded would look exactly like the person’s own on the
consent page. Step 7 first proves with two accounts that claude.ai ties its callback to
the account that started the flow, and adds hosted wording to the consent page.__Host- on https; plain on http, where browsers will not keep a __Host-
one), and Google’s callback finishes only in the browser holding it. The first arrival
claims the approval in the store, so a refused one (say, a forwarded link) cannot be
replayed from a request log; every sign-in also carries the process’s boot ID, so a
restart cannot revive one either. The connector itself keeps no access log. The MCP
spec requires consent of a connector that signs people in with one fixed Google client
for self-registered clients.same-origin, never no-referrer: under
no-referrer a browser posts the page’s own form with Origin: null and the Origin check
refuses it, so nobody can sign in. The unit tests could not see this; a review driving
real Chrome and Edge did. tests/connector_kit.py now sends the Origin a browser would.email_verified: true and an hd equal to the email’s domain. No hd means a personal
account made with a work address: refused. The Google code exchange uses PKCE.iss, equal character for character to the
metadata’s issuer (RFC 9207), and the metadata says so. That includes the refusals the
SDK’s /authorize sends itself, which our wrapper route completes. After the browser is
proven, every outcome reaches the client: a refusal page links back to it.--max-instances 1) until a shared
store replaces MemoryStore.LAB_OWNER_EMAIL’s accounts, so anyone else allowed to sign in gets whoami only.audit.record(),
never arguments, results, tokens or exception messages (an HTTP error’s message holds
the request URL, so a Gmail query). Failures log the exception type and file:line only. A
call the client abandons is recorded as cancelled, since its thread still reads. Every
refusal is recorded too: at registration, /authorize (the SDK’s own included), the
consent page, Google’s answer and /token. A failed Google sign-in records a fixed
sub-reason (Google’s error code, an ID-token check, the hd and email domains), never
Google’s own text, so the first live run can be diagnosed from the log alone.
Tests check the record as production writes it (as_written), not pytest’s caplog text,
which never shows the audit fields.python -m lab.connector.signer: 48 random bytes plus a
checksum, which a typed phrase cannot pass by accident. Anyone can get a signed value
from /register and test guesses offline, so the key must be random.mcp 2.x: the low-level Server takes on_list_tools / on_call_tool in its
constructor (the 1.x decorators are gone); types are snake_case with camelCase aliases.
streamable_http_app’s default host settings reject every non-localhost Host header,
which means everything behind Cloud Run (Gwen hit this).AnthropicVertex accepts middleware=. Vertex has no server-side refusal fallback, so
BetaRefusalFallbackMiddleware plus one BetaFallbackState per conversation does it.medium (set it), and
forced tool_choice returns 400.gw-admin-assistant 2026-10-08). The 5.5 models are served on us and
global only. Request quota in adam-cos-lab before relying on the Vertex loop.mcp 2.3 auth internals: custom routes are appended after the SDK’s own, so replacing an
SDK endpoint means inserting a route in front (app.router.routes[0:0]). Its /revoke
requires a client_secret field, so it refuses every public client, Claude included;
ours replaces it. Its metadata omits "none" and RFC 9207 iss support; ours adds both.
It never checks tool arguments against input_schema. A bare AnyHttpUrl gains a
trailing slash, while AuthSettings.issuer_url keeps none: build iss from the latter.https://claude.ai/api/mcp/auth_callback; the claude.com “twin” a support article
mentions answered 404 on 2026-10-15, so it is not a callback. Discovery, registration
and token calls get 10 s; refreshes get 30 s. Rotate refresh tokens for public clients;
answer a dead one with invalid_grant. Claude uses CIMD only if the metadata says so,
otherwise DCR.client_name “Claude Code (iss: _authorize_route in app.py adds it. Starlette redirects
/mcp/ to /mcp from the request’s own scheme (http behind Cloud Run’s TLS), so
redirect_slashes is off.hd is believed to be the Workspace organization’s domain. A person whose address
is on a secondary domain may then be refused as not_a_workspace_account. Unproven: check
on the first live sign-in, and before SCSP, which may need a configured set of domains.httpx2, not httpx, and refuses non-ASCII header
text: send such headers as latin-1 bytes in tests. A second browser on the same app is a
second TestClient used without with (the app’s lifespan may run only once).<venv python> -m pytest -q -p no:cacheprovider
op run --env-file=secrets.env.tpl -- <venv python> -m lab.cli
<venv python> -m lab.connector # the connector at http://localhost:8080/mcp
lab/connector/__init__.py maps the connector’s modules to the steps of the sign-in
journey; oauth.py’s docstring walks the journey itself. REVIEWS.md holds
every finding of the three security reviews, with its fix and the test that pins it:
read it before changing anything in lab/connector/.