SynContext — Anthropic Connectors Directory Submission Proof Pack¶
Service: SynContext (https://syncontext.dev)
Operator: Taino Software NJ — Edwin S Mejia ([email protected])
MCP transport endpoint: https://syncontext.dev/mcp
Documentation: https://syncontext.dev/docs
Privacy policy: https://syncontext.dev/privacy
Repository: https://github.com/manin809/SynContext (private; auditable on request)
Production HEAD at submission: 99dfc57e50a3c40e65644c6a41634fe5a3260a6d (99dfc57)
Submission date: 2026-05-13
Spec compliance target: MCP 2025-06-18 Authorization spec + RFC 7591 (DCR) + RFC 9728 (PRM) + RFC 8414 (AS metadata) + RFC 6750 (Bearer) + RFC 8707 (audience binding) + OAuth 2.1 (PKCE-only)
1. Executive Summary¶
SynContext is a hosted Model Context Protocol (MCP) server providing cross-AI context sharing as a managed SaaS. It exposes a remote MCP transport at https://syncontext.dev/mcp with OAuth 2.1 + PKCE authorization, RFC 7591 Dynamic Client Registration, RFC 9728 Protected Resource Metadata, and full CORS support for browser-originated calls from https://claude.ai and https://claude.com.
All technical requirements for the Anthropic Connectors Directory are met:
- ✅ OAuth 2.1 with PKCE (S256 challenge method,
token_endpoint_auth_method: none, no implicit grant) - ✅ HTTPS only (HSTS preload, TLS via Cloudflare CDN edge)
- ✅ CORS (preflight + response-side ACAO for Anthropic origins; SC-85-F1 extends the exact-match marketplace allowlist to OpenAI origins as historical/dormant support; OpenAI App Directory submission is deferred per Decision #67)
- ✅ MCP tool annotations (21 tools, all with
readOnlyHint/destructiveHint/idempotentHint/openWorldHint/title) - ✅ Dynamic Client Registration (RFC 7591, public endpoint
/oauth/register) - ✅ Protected Resource Metadata (RFC 9728, public endpoint
/.well-known/oauth-protected-resource; URL advertised inWWW-Authenticateper MCP 2025-06-18 §2) - ✅ Authorization Server Metadata (RFC 8414, public endpoint
/.well-known/oauth-authorization-server) - ✅ Audience binding (RFC 8707, opaque access tokens bound to
https://syncontext.dev/mcpresource URI via DBaudiencecolumn) - ✅ Token revocation (RFC 7009, public endpoint
/oauth/revoke)
The service is deployed on Railway Pro (US East region, us-east4-eqdc4a) on PostgreSQL 18.3. Stripe billing is live (Pro $12/mo, Team $29/mo, free tier available). 777 automated tests pass on every commit (Python 3.12 + 3.13 CI matrix; +6 deselected).
2. Verified Endpoint Captures (production HEAD 99dfc57)¶
All captures below were executed against the production deployment at https://syncontext.dev on 2026-05-13, against production HEAD 99dfc57. The OAuth surface was materially shipped via PR #57 (Decision #40 SC-75-G1G2 marketplace certification); consent.html copy was refined via PR #61 (SC-78-T4).
2.1 Authorization Server Metadata — RFC 8414¶
Request:
Response — HTTP 200:
{
"issuer": "https://syncontext.dev",
"authorization_endpoint": "https://syncontext.dev/oauth/authorize",
"token_endpoint": "https://syncontext.dev/oauth/token",
"revocation_endpoint": "https://syncontext.dev/oauth/revoke",
"registration_endpoint": "https://syncontext.dev/oauth/register",
"scopes_supported": ["read", "write", "admin"],
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"code_challenge_methods_supported": ["S256"],
"token_endpoint_auth_methods_supported": ["none"],
"service_documentation": "https://syncontext.dev/docs"
}
Compliance:
- issuer matches the canonical origin
- All advertised endpoints are live (verified §2.2-§2.4 below)
- code_challenge_methods_supported: ["S256"] — PKCE-only, S256-only per Decision #14 / Entry #280 §1
- response_types_supported: ["code"] — no implicit grant per Entry #280 §3
- token_endpoint_auth_methods_supported: ["none"] — PKCE-only, no client secret per Entry #280 §2
2.2 Protected Resource Metadata — RFC 9728¶
Request:
Response — HTTP 200:
{
"resource": "https://syncontext.dev/mcp",
"authorization_servers": ["https://syncontext.dev"],
"scopes_supported": ["read", "write", "admin"],
"bearer_methods_supported": ["header"],
"resource_documentation": "https://syncontext.dev/docs"
}
Compliance:
- resource exactly matches the MCP transport URI (audience-binding canonical URI per RFC 8707)
- authorization_servers correctly references the issuer from §2.1
- bearer_methods_supported: ["header"] — Authorization: Bearer only, no query-param exposure
2.3 WWW-Authenticate on /mcp 401 — MCP 2025-06-18 §2 MUST¶
Request (no credential, live verified 2026-05-13T11:20:16Z):
Response — HTTP 401:
HTTP/1.1 401 Unauthorized
www-authenticate: Bearer realm="SynContext", resource_metadata="https://syncontext.dev/.well-known/oauth-protected-resource"
x-railway-request-id: tA-VpBTIQm-YnoWlO8poTA
x-request-id: c9387f63706c4d64
Request (invalid bearer credential):
POST https://syncontext.dev/mcp
Authorization: Bearer <redacted-invalid-token>
Content-Type: application/json
{}
Response — HTTP 401:
HTTP/2 401
www-authenticate: Bearer realm="SynContext", resource_metadata="https://syncontext.dev/.well-known/oauth-protected-resource", error="invalid_token"
Compliance:
- MCP 2025-06-18 §2 MUST: WWW-Authenticate advertises resource_metadata URL pointing to the PRM document from §2.2
- RFC 9728 §5.1: parameter named exactly resource_metadata
- RFC 6750 §3 challenge ordering: realm → resource_metadata → conditional error="invalid_token"
- /api/* REST endpoints intentionally do NOT advertise resource_metadata (REST API is not an OAuth resource server per MCP spec); regression-tested by TC3 and live-verified at §2.4 below.
- Implementation lives at context_hub/auth.py:_send_401 (lines 429-459); routing via context_hub/__main__.py (lines 449-460). NOT in context_hub/server.py — server.py only hosts the tool definitions.
2.4 /api/* 401 Regression Guard (live verified 2026-05-13T11:20:16Z)¶
Request:
Response — HTTP 401:
HTTP/1.1 401 Unauthorized
www-authenticate: Bearer realm="SynContext"
x-railway-request-id: 5hqZqeZSQF66hMIkO8poTA
x-request-id: 181ea3280b964406
Compliance:
- /api/* 401 emits Bearer realm="SynContext" ONLY (no resource_metadata). MCP-spec resource metadata is scoped to the MCP transport endpoint, not REST API endpoints.
- Path-specific behavior is enforced at context_hub/auth.py:_send_401 via path.startswith("/mcp") branch (line 429-459).
- Regression test: tests/test_auth_middleware.py TC3 and TC12.
2.5 CORS Preflight for Anthropic Origins¶
https://claude.ai (live verified 2026-05-13T11:20:16Z)¶
Request:
OPTIONS https://syncontext.dev/mcp
Origin: https://claude.ai
Access-Control-Request-Method: POST
Access-Control-Request-Headers: authorization,content-type,mcp-protocol-version
Response — HTTP 204:
HTTP/1.1 204 No Content
access-control-allow-origin: https://claude.ai
access-control-allow-methods: GET, POST, DELETE, OPTIONS
access-control-allow-headers: authorization, content-type, mcp-protocol-version, mcp-session-id
access-control-max-age: 86400
vary: Origin
x-railway-request-id: 2OTBzrBiRtqCyf5XBT7zVQ
x-request-id: 4ebbbffb8b5440e0
https://claude.com¶
Request:
OPTIONS https://syncontext.dev/mcp
Origin: https://claude.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: authorization,content-type,mcp-protocol-version
Response — HTTP 204:
HTTP/2 204
access-control-allow-origin: https://claude.com
access-control-allow-methods: GET, POST, DELETE, OPTIONS
access-control-allow-headers: authorization, content-type, mcp-protocol-version, mcp-session-id
access-control-max-age: 86400
vary: Origin
Disallowed origin regression test¶
Request:
Response — HTTP 403: (no Access-Control-Allow-Origin header emitted; browser will reject)
Compliance:
- Exact-match origin allowlist via Python frozenset lookup (no wildcards, no substring matching, no header injection vector)
- Allowed methods cover MCP transport verbs: GET (SSE keepalive), POST (tool calls), DELETE (session termination), OPTIONS (preflight)
- Allowed headers cover MCP transport spec: authorization (bearer), content-type, mcp-protocol-version, mcp-session-id
- Vary: Origin set per HTTP caching spec for origin-dependent responses
- Implementation lives at context_hub/__main__.py:257-353 (path-scoped /mcp CORS wrapper).
SC-85-F1 OpenAI App Directory addendum (historical / deferred per Decision #67): The same exact-match, dual-layer
CORS model extends to https://chatgpt.com and https://chat.openai.com.
This addendum does not alter the dated 2026-05-13 Anthropic captures above;
it is retained as historical implementation evidence only. Future OpenAI submission evidence
must capture fresh post-deploy preflight results for both OpenAI origins if Edwin explicitly reopens the App Directory track.
2.6 Dynamic Client Registration — RFC 7591¶
Request:
POST https://syncontext.dev/oauth/register
Content-Type: application/json
{
"client_name": "SynContext Marketplace Proof Pack",
"redirect_uris": ["https://claude.ai/api/mcp/auth_callback"]
}
Response — HTTP 201 Created:
{
"client_id": "yZDEC78guOzHzQtcwYCIY8afedDBe3nE",
"client_name": "SynContext Marketplace Proof Pack",
"redirect_uris": ["https://claude.ai/api/mcp/auth_callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"scope": "",
"token_endpoint_auth_method": "none",
"created_at": "2026-05-13T00:33:33.930309+00:00"
}
Compliance:
- RFC 7591 §3.2.1: HTTP 201 Created on successful registration
- RFC 7591 §3.2: redirect_uris echoed exactly (no URL transformation)
- token_endpoint_auth_method: "none" — PKCE-only public client per Decision #14 / Entry #280 §2
- Default grant set: authorization_code + refresh_token (no implicit, no client_credentials)
- Default response type: code only
- client_id is an opaque random 32-character URL-safe string
2.7 SSE Keepalive (Antigravity / future MCP notification clients)¶
Request:
Response — HTTP 200:
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
: heartbeat
: heartbeat
Confirmed by independent Codex empirical capture (SynContext entry #1775 §2).
2.8 Operational endpoints (live verified at HEAD 99dfc57)¶
/health:
GET https://syncontext.dev/health
→ HTTP 200 {"status":"ok","service":"syncontext","db":"connected"}
/version: (deployed commit traceability)
GET https://syncontext.dev/version
→ HTTP 200 {"service":"syncontext","commit":"99dfc57","commit_full":"99dfc57e50a3c40e65644c6a41634fe5a3260a6d","branch":"main"}
/privacy:
/docs/: (canonical documentation URL; bare /docs 301-redirects to trailing-slash)
2.9 Audience Binding Code Path (RFC 8707)¶
Audience binding is enforced at 3 distinct code points at HEAD 99dfc57:
- Authorization request validation —
context_hub/oauth_handlers.py:203-207returnsinvalid_targetif the request'sresourceparameter does not equalconfig.OAUTH_CANONICAL_URI(https://syncontext.dev/mcp). - DB propagation —
context_hub/db/operations.py:3989-4024readsresourcefrom the authorization code row and inserts it intooauth_access_tokens.audienceandoauth_refresh_tokens.audienceduring code-for-token exchange. - Resource-server enforcement —
context_hub/auth.py:151-188validatesmcp_at_*tokens only on the/mcppath and rejects with HTTP 401 ifaccess_row["audience"] != config.OAUTH_CANONICAL_URI.
Terminology precision: SynContext uses opaque tokens (not JWT). The audience is enforced as a DB column on the oauth_access_tokens and oauth_refresh_tokens tables (audience binding). It is NOT exposed as a JWT-style aud field in the token response body. The /oauth/token response returns only {token_type, access_token, refresh_token, expires_in, scope} — no audience field reaches clients. Audience proof therefore requires a server-side DB lookup (see §2.11 audience binding query).
2.10 Railway DB v14 Production Verification¶
context_hub/db/operations.py:544-639 contains the PG v14 additive DDL block that catches individual DDL exceptions (logged warnings at lines 565, 578, 598, 615, 633) while still upserting schema_meta.version = SCHEMA_VERSION at lines 635-639. Production proof therefore requires verifying actual table and index existence — the schema_meta.version row alone is insufficient.
Canonical v14 OAuth tables (per context_hub/db/schema.py:609-680):
- oauth_clients
- oauth_cimd_cache
- oauth_authorization_codes
- oauth_access_tokens
- oauth_refresh_tokens
Canonical v14 OAuth indexes:
- idx_oauth_clients_ip
- idx_oauth_cimd_expires
- idx_oauth_codes_expires
- idx_oauth_at_user
- idx_oauth_at_expires
- idx_oauth_rt_user
- idx_oauth_rt_parent
Operator verification queries (read-only psql against Railway production; SELECT-only, no writes):
-- Q1. Context stamp: no secrets, no row data.
SELECT current_database() AS database_name, current_user AS database_user, now() AS checked_at;
-- Q2. Schema version (necessary but not sufficient).
SELECT value AS schema_version FROM public.schema_meta WHERE key = 'version';
-- Q3. Expected v14 OAuth tables.
WITH expected(table_name) AS (
VALUES
('oauth_clients'),
('oauth_cimd_cache'),
('oauth_authorization_codes'),
('oauth_access_tokens'),
('oauth_refresh_tokens')
)
SELECT
table_name,
CASE WHEN to_regclass('public.' || table_name) IS NULL THEN 'MISSING' ELSE 'OK' END AS status,
CASE WHEN to_regclass('public.' || table_name) IS NULL THEN NULL ELSE pg_total_relation_size(to_regclass('public.' || table_name)) END AS total_bytes
FROM expected
ORDER BY table_name;
-- Q4. Expected v14 OAuth indexes.
WITH expected(index_name, table_name) AS (
VALUES
('idx_oauth_clients_ip', 'oauth_clients'),
('idx_oauth_cimd_expires', 'oauth_cimd_cache'),
('idx_oauth_codes_expires', 'oauth_authorization_codes'),
('idx_oauth_at_user', 'oauth_access_tokens'),
('idx_oauth_at_expires', 'oauth_access_tokens'),
('idx_oauth_rt_user', 'oauth_refresh_tokens'),
('idx_oauth_rt_parent', 'oauth_refresh_tokens')
)
SELECT
e.index_name,
e.table_name,
CASE WHEN i.indexname IS NULL THEN 'MISSING' ELSE 'OK' END AS status,
i.indexdef
FROM expected e
LEFT JOIN pg_indexes i
ON i.schemaname = 'public'
AND i.indexname = e.index_name
ORDER BY e.table_name, e.index_name;
-- Q5. Column-level shape, schema only.
SELECT table_name, ordinal_position, column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name IN (
'oauth_clients',
'oauth_cimd_cache',
'oauth_authorization_codes',
'oauth_access_tokens',
'oauth_refresh_tokens'
)
ORDER BY table_name, ordinal_position;
-- Q6. Optional count-only proof. Counts reveal volume but not token/user data.
SELECT 'oauth_clients' AS table_name, COUNT(*) AS row_count FROM public.oauth_clients
UNION ALL SELECT 'oauth_cimd_cache', COUNT(*) FROM public.oauth_cimd_cache
UNION ALL SELECT 'oauth_authorization_codes', COUNT(*) FROM public.oauth_authorization_codes
UNION ALL SELECT 'oauth_access_tokens', COUNT(*) FROM public.oauth_access_tokens
UNION ALL SELECT 'oauth_refresh_tokens', COUNT(*) FROM public.oauth_refresh_tokens
ORDER BY table_name;
Operator output — captured 2026-05-13T15:30Z by Edwin S Mejia against Railway production (database railway, user postgres):
Q1 — Context stamp:
database_name | database_user | checked_at
---------------+---------------+-------------------------------
railway | postgres | 2026-05-13 15:30:28.808474+00
(1 row)
Q2 — Schema version:
Q3 — Expected v14 OAuth tables:
table_name | status | total_bytes
---------------------------+--------+-------------
oauth_access_tokens | OK | 32768
oauth_authorization_codes | OK | 24576
oauth_cimd_cache | OK | 24576
oauth_clients | OK | 49152
oauth_refresh_tokens | OK | 32768
(5 rows)
All 5 canonical v14 OAuth tables present, byte sizes non-NULL (initialized).
Q4 — Expected v14 OAuth indexes:
index_name | table_name | status | indexdef
-------------------------+---------------------------+--------+---------------------------------------------------------------------------------------------------
idx_oauth_at_expires | oauth_access_tokens | OK | CREATE INDEX idx_oauth_at_expires ON public.oauth_access_tokens USING btree (expires_at)
idx_oauth_at_user | oauth_access_tokens | OK | CREATE INDEX idx_oauth_at_user ON public.oauth_access_tokens USING btree (user_id)
idx_oauth_codes_expires | oauth_authorization_codes | OK | CREATE INDEX idx_oauth_codes_expires ON public.oauth_authorization_codes USING btree (expires_at)
idx_oauth_cimd_expires | oauth_cimd_cache | OK | CREATE INDEX idx_oauth_cimd_expires ON public.oauth_cimd_cache USING btree (expires_at)
idx_oauth_clients_ip | oauth_clients | OK | CREATE INDEX idx_oauth_clients_ip ON public.oauth_clients USING btree (registered_by_ip)
idx_oauth_rt_parent | oauth_refresh_tokens | OK | CREATE INDEX idx_oauth_rt_parent ON public.oauth_refresh_tokens USING btree (parent_refresh_hash)
idx_oauth_rt_user | oauth_refresh_tokens | OK | CREATE INDEX idx_oauth_rt_user ON public.oauth_refresh_tokens USING btree (user_id)
(7 rows)
All 7 canonical v14 OAuth indexes present with expected indexdef.
Q5 — Column-level shape (45 rows total; abbreviated below):
Key columns relevant to audience binding (RFC 8707):
oauth_access_tokens.audience—text NOT NULL(ordinal 5)oauth_refresh_tokens.audience—text NOT NULL(ordinal 6)oauth_authorization_codes.resource—text NOT NULL(ordinal 6) — input parameter that propagates to token rows
Full column shape for oauth_access_tokens (the audience-binding target):
table_name | ordinal_position | column_name | data_type | is_nullable
---------------------------+------------------+----------------------------+--------------------------+-------------
oauth_access_tokens | 1 | token_hash | text | NO
oauth_access_tokens | 2 | client_id | text | NO
oauth_access_tokens | 3 | user_id | text | NO
oauth_access_tokens | 4 | scope | text | NO
oauth_access_tokens | 5 | audience | text | NO
oauth_access_tokens | 6 | expires_at | timestamp with time zone | NO
oauth_access_tokens | 7 | revoked | boolean | NO
oauth_access_tokens | 8 | created_at | timestamp with time zone | NO
Full Q5 output (45 rows across 5 tables) retained operator-side as q5-column-shape-2026-05-13.txt.
Q6 — Row counts (snapshot 2026-05-13T15:30Z, pre-Claude.ai E2E test):
table_name | row_count
---------------------------+-----------
oauth_access_tokens | 0
oauth_authorization_codes | 0
oauth_cimd_cache | 0
oauth_clients | 4
oauth_refresh_tokens | 0
(5 rows)
oauth_clients count of 4 reflects DCR registrations from prior testing (S78 proof-pack DCR yZDEC78guOzHzQtcwYCIY8afedDBe3nE + 3 antigravity-client test entries from S69). oauth_access_tokens count of 0 confirms no production user OAuth flows yet completed at snapshot time. Post-snapshot at 16:08Z (after the Claude.ai E2E test in §2.11 below), oauth_access_tokens count became 1 and oauth_clients count became 5 — see §2.11 evidence rows.
Verdict: Railway PG v14 production schema verified. All 5 canonical OAuth tables present, all 7 canonical indexes present, audience column type and constraint match RFC 8707 binding requirement.
2.11 Connector Callback Redirect (Claude.ai / Desktop / Code)¶
The Anthropic Connectors Directory requires evidence that the SynContext OAuth flow completes end-to-end via each Claude client surface. The capture plan below uses Claude.ai web as the PRIMARY HAR-bearing client and Claude Desktop + Claude Code as SHORTER corroborating witnesses.
Required visible/evidenced fields (across all three clients):
- resource=https://syncontext.dev/mcp — audience-binding canonical URI in authorize URL
- redirect_uri — must exactly match the client's registered callback
- scope — canonical scope names (read / write / admin)
- code_challenge_method=S256 — PKCE
- Token response shape: {token_type:"Bearer", expires_in:<N>, scope:"..."} with token VALUES redacted
- Audience binding — NOT visible in opaque token response; proved via server-side DB lookup (see audience query below)
Capture artifact set:
- Claude.ai (web, PRIMARY) — one sanitized HAR + 3-5 screenshots:
- Initiate connector → OAuth authorize page/consent
- Redirect back to client callback with
code+state POST /oauth/token200 response- First
/mcpprobe / tool-list request success -
HAR file referenced by SHA256 + filename; raw HAR retained operator-side (NOT committed to repo)
-
Claude Desktop (corroborating) — screenshot + log lines:
- Screenshot of configured/connected SynContext connector
- Log line(s) showing successful OAuth completion + first MCP connection
-
Server URL
https://syncontext.dev/mcpvisible -
Claude Code (corroborating) — terminal transcript:
- Connector add/auth flow completion
- Read-only MCP list/probe succeeding
- Configured endpoint visible
Audience binding DB lookup (operator-only, in same Railway psql session as §2.10):
After Edwin completes the Claude.ai web OAuth flow and captures the token response, he computes the SHA256 of the access token locally (procedure in §3.3 below; plaintext token NEVER pasted into proof artifacts) and runs:
-- Replace <TOKEN_SHA256> with the locally-computed SHA256 of the access token.
SELECT client_id, scope, audience, revoked, expires_at
FROM public.oauth_access_tokens
WHERE token_hash = '<TOKEN_SHA256>';
Expected: exactly 1 row with audience = 'https://syncontext.dev/mcp' and revoked = false.
Operator output — captured 2026-05-13 between 15:45Z and 16:09Z by Edwin S Mejia:
Capture sequence:
- Edwin opened Settings → Connectors in
https://claude.ai(logged-in account: [email protected]) - "Add custom connector" with
Name: SynContext,Remote MCP server URL: https://syncontext.dev/mcp - SynContext consent screen displayed: "Authorize Access — Claude is requesting access to your SynContext data with the following permissions: read / write / admin"
- Edwin clicked "Authorize" → redirect back to
https://claude.ai→ connector enrolled as "Connected" - New chat: "Use the SynContext tool to list my projects" → Claude executed
hub_list_projectsand returned the 6 active SynContext projects (syncontext,smoke-test-s16,sports-intelligence-mcp,aegis-v01,smart-sport-analytics,example) — confirming end-to-end OAuth-authenticated MCP tool call success against production HEAD99dfc57
DCR registration evidence (oauth_clients snapshot 2026-05-13T16:09Z):
client_id | client_name | created_at
----------------------------------+-----------------------------------+-------------------------------
mHTN8GLhHDsOPLJrNdwKzr22FkjDTBOQ | Claude | 2026-05-13 15:45:29.190749+00
yZDEC78guOzHzQtcwYCIY8afedDBe3nE | SynContext Marketplace Proof Pack | 2026-05-12 00:33:33.930309+00
VmJQ2AsjuxqId_JGBe0O2h8GoaXmvwyy | antigravity-client | 2026-04-27 22:18:13.742937+00
QWwaNzNcPB3NnUXhkBLlXoOoXLviL7sI | antigravity-client | 2026-04-27 22:18:00.933204+00
3h9ynC2sCbp3aTuhEVaOTyyF1a_oDOAm | antigravity-client | 2026-04-27 22:17:59.677783+00
(5 rows)
The first row (client_name: Claude, client_id mHTN8GLhHDsOPLJrNdwKzr22FkjDTBOQ) is the DCR registration auto-generated by claude.ai during connector setup at 15:45:29Z — RFC 7591 §3.2.1 confirmed (token_endpoint_auth_method = "none", PKCE-only public client per Decision #14 / Entry #280 §2).
Audience binding evidence (oauth_access_tokens snapshot 2026-05-13T16:09Z, post-MCP-tool-call):
client_id | scope | audience | revoked | expires_at | created_at
----------------------------------+------------------+----------------------------+---------+-------------------------------+-------------------------------
mHTN8GLhHDsOPLJrNdwKzr22FkjDTBOQ | read write admin | https://syncontext.dev/mcp | f | 2026-05-13 17:08:18.194801+00 | 2026-05-13 16:08:18.194801+00
(1 row)
The access token issued to Edwin's Claude.ai connector (client_id mHTN8GLhHDsOPLJrNdwKzr22FkjDTBOQ) was persisted with audience = https://syncontext.dev/mcp (canonical MCP resource URI) and revoked = false. Token TTL: 1 hour (16:08:18Z → 17:08:18Z). Scope: read write admin per consent granted. The token_hash column is intentionally omitted from this evidence (SHA256 of the opaque access token; not committable).
RFC 8707 audience binding chain proven E2E:
- ✅
oauth_clients.client_id mHTN8GLhHDsOPLJrNdwKzr22FkjDTBOQ— DCR registration by Claude.ai (15:45:29Z) - ✅ Authorize request received
resource=https://syncontext.dev/mcp→ propagated intooauth_authorization_codes.resourcepercontext_hub/oauth_handlers.py:203-207 - ✅
/oauth/tokenexchange propagatedresource→audiencecolumn onoauth_access_tokenspercontext_hub/db/operations.py:3989-4024(issued 16:08:18Z) - ✅
audience = https://syncontext.dev/mcpmatchesconfig.OAUTH_CANONICAL_URIpercontext_hub/auth.py:151-188resource-server enforcement - ✅ MCP tool call success (
hub_list_projectsreturned 6 projects) confirms the audience-bound token authenticates successfully against/mcpendpoint
Architecture note on client-side Bearer header capture:
The Authorization: Bearer mcp_at_* header is NOT directly visible in browser DevTools when using the Claude.ai web client. The MCP transport flow is:
The OAuth access token is held server-side in Anthropic's infrastructure (as designed by the MCP authorization spec for hosted client deployments). Edwin's browser HAR therefore does not contain the Bearer header. Direct Bearer-header capture would require a non-claude.ai client (Claude Desktop or Claude Code, both of which originate MCP requests directly to syncontext.dev/mcp) or a server-side log inspection of Anthropic's egress — neither of which is required to prove audience binding given the canonical DB evidence above.
Artifact set retained operator-side (not committed per §3.1 redaction prohibition):
claude-ai-syncontext-tool-call.har— sanitized HAR from claude.ai web during thehub_list_projectstool call (Chrome 130+ default sanitized export excludesCookie,Set-Cookie,Authorizationheaders); contains only browser ↔ claude.ai traffic (no directsyncontext.dev/mcprequests since flow is server-side proxied by Anthropic)- Screenshots: SynContext authorize consent screen, connector "Connected" state in Settings, chat showing successful tool call result with the 6 SynContext projects listed
Available on request from operator workstation; SHA256 hashes computed at retention time per §3.3 procedure.
3. Operator Runbook — Edwin Capture Procedure¶
Tracker #3 closes when Edwin appends the §2.10 + §2.11 operator outputs via a fix-forward commit. This section codifies the redaction discipline and SHA256 procedure required to keep secrets out of the repo.
3.1 Strict redaction prohibition¶
The following values MUST NEVER appear in committed repo files, SynContext entries, or any proof artifact:
- Plaintext
access_tokenvalues - Plaintext
refresh_tokenvalues - Plaintext authorization
codevalues from the callback redirect - Plaintext
code_verifiervalues - Browser cookies (any
Cookie:orSet-Cookie:header values) Authorization: Bearer <token>header values- Full reusable token identifiers (e.g., complete
mcp_at_xxxxx...strings) - Plaintext session identifiers that grant access
3.2 Values safe to commit¶
client_id(public per RFC 7591 §3.2)redirect_uri(URL only)scope(canonical scope names)code_challenge_method=S256(constant)statevalue SHA256-hashed (not raw)resource=https://syncontext.dev/mcp(canonical URI)- Token response shape:
{token_type:"Bearer", expires_in:<N>, scope:"..."}with token VALUES redacted x-request-id+x-railway-request-id(audit traceability only; no auth surface)- SHA256 hash of token (Edwin computes locally) for DB audience lookup join
- DB row outputs showing
client_id,scope,audience,revoked, expiry — TOKEN COLUMN OMITTED
3.3 SHA256 procedure (operator local-only, plaintext NEVER leaves machine)¶
PowerShell:
[BitConverter]::ToString(
[Security.Cryptography.SHA256]::Create().ComputeHash(
[Text.Encoding]::UTF8.GetBytes("<plaintext_access_token>")
)
).Replace("-","").ToLower()
bash / zsh:
The plaintext token NEVER touches the proof pack, SynContext entries, or any commit. Only the SHA256 hex digest + DB lookup join result.
4. Security Baseline¶
Every response from the production deployment includes the following security headers (verified across all endpoint captures in §2):
| Header | Value | Purpose |
|---|---|---|
strict-transport-security |
max-age=63072000; includeSubDomains; preload |
HSTS preload (2 years) |
x-content-type-options |
nosniff |
MIME-type sniff protection |
x-frame-options |
DENY |
Clickjacking protection |
referrer-policy |
strict-origin-when-cross-origin |
Referrer minimization |
permissions-policy |
camera=(), microphone=(), geolocation=() |
Feature policy denial |
x-xss-protection |
1; mode=block |
Legacy XSS filter |
content-security-policy |
default-src 'self'; ... |
CSP with strict default-src |
x-request-id |
per-request UUID | request tracing / debugging |
Route-Specific CSP Architecture: F3 implemented strict, path-based Content Security Policy (CSP) isolation. All application, API, and OAuth transactional routes (/, /app/, /api/*, /oauth/*, /.well-known/*, /mcp) enforce strict CSP with no unsafe-inline directives, directly mitigating XSS risks on active execution paths. A scoped exception exists solely for the /docs/* route to support static MkDocs Material styling, bounded by ASGI middleware dispatch and enforced via comprehensive route-policy test coverage. There is no user input reflection on the documentation path, effectively eliminating the injection vector.
5. Infrastructure & Deployment¶
| Layer | Detail |
|---|---|
| Application runtime | Python 3.12+ (CI matrix: 3.12 + 3.13) |
| MCP framework | FastMCP SDK |
| HTTP framework | Starlette + uvicorn |
| Database (production) | PostgreSQL 18.3 (Debian 18.3-1.pgdg13+1) on x86_64-pc-linux-gnu, compiled by gcc 14.2.0, 64-bit |
| Database driver | asyncpg |
| Hosting | Railway Pro |
| Region | us-east4-eqdc4a (per x-railway-edge response header on all captures) |
| CDN / TLS | Cloudflare (verified by cf-ray / server: cloudflare headers) |
| DNS | Cloudflare DNS |
| Resend | |
| Billing | Stripe LIVE (Pro $12/mo, Team $29/mo) |
| Crypto at rest | Fernet (AES-128-CBC + HMAC-SHA256) on user-content fields |
| Auth credentials | bcrypt password hashing, SHA-256 API keys, opaque session tokens |
6. MCP Tool Surface¶
The MCP server exposes 21 tools, all with full annotations per MCP 2025-06-18 spec:
- Each tool declares
readOnlyHint,destructiveHint,idempotentHint,openWorldHint, andtitle - All tools use flat parameters (ChatGPT Connectors compatibility)
- All tools are wrapped in
@safe_tooldecorator (exception-safe error envelope) - Tier-gated capabilities use an allowlist model (not blocklist) per security policy
- Tool count invariant verified post-deploy:
len(mcp._tool_manager._tools) == 21
7. Test Coverage¶
| Metric | Value |
|---|---|
| Total automated tests | 777 passed, 6 deselected (CI green on Python 3.12 + 3.13) |
| New marketplace-compliance tests in PR #57 | 14 (TC1-TC14) |
| Test files added/modified in PR #57 | tests/test_auth_oauth_bearer.py (TC1, TC2, TC14), tests/test_auth_middleware.py (TC3, TC12), tests/test_mcp_oauth_transport.py (TC4-TC11, TC13) |
CI workflows (all green at HEAD 99dfc57) |
test (3.12), test (3.13), ruff, dashboard-dist-drift, GitGuardian Security Checks |
Highlighted falsification coverage:
- TC3 + TC12 — regression guard: /api/* 401 never leaks resource_metadata; /api/* OPTIONS not affected by /mcp CORS bypass
- TC6 + TC9 — exact-match origin enforcement (no wildcard, no substring, no header injection via CRLF)
- TC10 — case-insensitive HTTP method handling (scope.get("method", "").upper())
- TC11 — non-OPTIONS methods (TRACE, CONNECT) cannot exploit the CORS preflight branch
- TC13 — end-to-end PRM roundtrip: parse resource_metadata from WWW-Authenticate → fetch URL → confirm resource field matches canonical URI
- TC14 — dynamic URL derivation via urlsplit(config.OAUTH_CANONICAL_URI), not hardcoded literal
8. Governance & Change Management¶
SynContext development follows a documented multi-AI governance model with:
- 3 specialist advisors reviewing changes by lane (Codex for code, Gemini for architecture/security, Kimi K2.6 for precision/falsification)
- Reinforced consensus protocol for High-Risk Files (3-of-3 pre-execution + 2-of-3 post-execution)
- Decision log with binding rationale for architectural choices (44 decisions logged through S78; canonical binding range Decisions #20-#44)
- Repository-anchored verification (every claimed line number / blob SHA verified live via
hub_github_read) - PR-only changes (no direct commits to
main); CI must be green before merge
The marketplace compliance changes in PR #57 (the SC-75-G1G2 sprint) went through the full T1 + HRF reinforced consensus protocol: - Investigation: SynContext entry #1776 - Pre-execution reviews: Codex #1778, Gemini #1777, Kimi K2.6 #1779 - Director synthesis: #1780 - Prompt review (sole reviewer Codex): #1781, #1782 - Execution: Claude Code completion #1784 - Post-execution verification: Codex #1785, Kimi #1786 (both APPROVED FOR MERGE) - Director closure: #1787 - Decision logged: Decision #40 — "SC-75-G1G2 MCP Marketplace WWW-Authenticate resource_metadata + Anthropic CORS Compliance"
Subsequent marketplace-relevant decisions: - Decision #38 §4 Tracker closures including SC-76-T9 (rate-limit 429 Retry-After compliance, PR #58) - Decision #43 — FIX Byte-Landing Audit framework hardening - Decision #44 — Procedural Typography Discipline for non-FIX executor prose
9. Submission Deliverables Quick Reference¶
| Field | Value |
|---|---|
| Service name | SynContext |
| MCP transport URL | https://syncontext.dev/mcp |
| OAuth Authorization Server | https://syncontext.dev |
| AS metadata | https://syncontext.dev/.well-known/oauth-authorization-server |
| PRM | https://syncontext.dev/.well-known/oauth-protected-resource |
| Registration endpoint (DCR) | https://syncontext.dev/oauth/register |
| Authorization endpoint | https://syncontext.dev/oauth/authorize |
| Token endpoint | https://syncontext.dev/oauth/token |
| Revocation endpoint | https://syncontext.dev/oauth/revoke |
| Documentation | https://syncontext.dev/docs/ |
| Privacy policy | https://syncontext.dev/privacy |
| Operator email | [email protected] |
| Production HEAD at submission | 99dfc57e50a3c40e65644c6a41634fe5a3260a6d |
| Allowed CORS origins (Anthropic capture) | https://claude.ai, https://claude.com |
| Historical additional CORS origins after SC-85-F1 (OpenAI App Directory deferred per Decision #67) | https://chatgpt.com, https://chat.openai.com |
Prepared by Claude (Director) for Edwin S Mejia / Taino Software NJ — 2026-05-13. All captures in §2.1-§2.8 reproducible from operator workstation. §2.10 + §2.11 operator outputs to be appended via fix-forward commit post-merge per Tracker #3 closure protocol.
OAuth Schema — Live Production Existence Proof (Gate 2, Tracker #8)¶
- Date: 2026-05-25T14:00:22Z UTC
- Method: scripts/smoke_oauth.py (merged PR #85, commit 6942936; SELECT-only schema-metadata smoke) run against the live Railway production PostgreSQL (US East) via the public proxy endpoint. DSN withheld (secret).
- Result:
- schema_meta.version = 15 (expected 15) — PASS
- 6 OAuth tables present: oauth_clients, oauth_cimd_cache, oauth_authorization_codes, oauth_access_tokens, oauth_refresh_tokens, oauth_user_grants — PASS
- 9 OAuth indexes present: idx_oauth_clients_ip, idx_oauth_cimd_expires, idx_oauth_codes_expires, idx_oauth_at_user, idx_oauth_at_expires, idx_oauth_rt_user, idx_oauth_rt_parent, idx_oauth_grants_user, idx_oauth_grants_active — PASS
Raw output: SynContext OAuth schema smoke proof (2026-05-25T14:00:22Z UTC) [PASS] schema_meta.version: expected '15'; observed '15' [PASS] OAuth tables: expected 6; missing none [PASS] OAuth indexes: expected 9; missing none SUMMARY: PASS
Tracker #8 (production existence proof) closed by this evidence. The fail-fast missing-table guard (operations.py:678-696) shipped S86 (2eda96c / D#53).