Bagaimana custom connector Claude menemukan server OAuth osFoundry, dan kenapa akses dicek di setiap panggilan? A developer documented how a Claude custom connector discovers the osFoundry remote MCP server's OAuth flow through the WWW-Authenticate header returned on unauthenticated tool calls. The server exposes its initialize handshake and a 59-tool catalog publicly, while tools/call returns a 401 pointing to RFC 9728 protected-resource metadata; each tool declares its required OAuth scope (osfoundry:read, write, or run) and readOnlyHint annotations. The writeup notes that resource metadata must be fetched from the header rather than constructed, since appending the well-known suffix to the resource URL returns a 404. Ini yang diterima client MCP remote saat memanggil tool osFoundry https://osfoundry.io/id tanpa token. Request-nya dikirim dengan curl dari sebuah Mac di Tokyo pada 2026-11-06: bash $ curl -s -D - -X POST https://api.osfoundry.io/api/v1/external-mcp \ -H 'content-type: application/json' \ -H 'accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":3,"method":"tools/call", "params":{"name":"list project resources","arguments":{}}}' HTTP/2 401 www-authenticate: Bearer resource metadata="https://api.osfoundry.io/.well-known/oauth-protected-resource/api/v1/external-mcp", error="invalid token", error description="Connect osFoundry to continue" {"jsonrpc":"2.0","id":3,"result":{"isError":true, "content": {"type":"text","text":"Authentication required"} , " meta":{"mcp/www authenticate": "Bearer resource metadata=\"https://api.osfoundry.io/.well-known/oauth-protected-resource/api/v1/external-mcp\", ..." }}} Satu header itu sudah cukup bagi custom connector Claude, atau koneksi MCP di developer mode ChatGPT, untuk menemukan halaman login. Pengguna cukup menempelkan URL-nya. Semua yang ada di bawah berasal dari request tanpa autentikasi ke endpoint live, ditambah rebuild kecil dalam Python yang dijalankan secara lokal untuk mengukur bagian yang tidak bisa diperlihatkan curl: apa yang terjadi pada akses setelah login. Tiga panggilan, tiga jawaban berbeda: | Request | Status | Yang dikembalikan | |---|---|---| | initialize | 200 | protokol 2025-06-18 , server osfoundry 0.1.0 , plus instruksi | | tools/list | 200 | 59 tool, 45,368 byte | | tools/call | 401 | WWW-Authenticate yang menunjuk ke resource metadata | | GET ke endpoint | 405 | tidak ada stream SSE lewat GET | Handshake dan katalog tool bersifat publik. Hanya pemanggilan tool yang butuh token. Dengan begitu host bisa menunjukkan ke pengguna tool apa saja yang ada sebelum siapa pun login. Tidak ada data pengguna di katalog, dan string instructions di initialize bersifat umum: Use list granted resources first. Every Note edit must use the exact tab ID. Local-only data is unavailable to cloud clients. Setiap tool di tools/list mendeklarasikan scope OAuth yang dibutuhkannya dan apakah tool itu hanya membaca: {"name": "edit record item", "annotations": {"readOnlyHint": false, "destructiveHint": true, "openWorldHint": false}, "securitySchemes": {"type": "oauth2", "scopes": "osfoundry:write" } } Dari 59 tool: 30 butuh osfoundry:read dan ditandai readOnlyHint: true , 26 butuh osfoundry:write , dan 3 butuh osfoundry:run request session run , run graph session , cancel graph run . Host bisa memakai readOnlyHint untuk menentukan panggilan mana yang perlu prompt persetujuan. resource metadata mengikuti RFC 9728 OAuth Protected Resource Metadata . Suffix well-known diletakkan tepat setelah host, lalu path resource ditambahkan di belakangnya. Suffix itu tidak ditempel di ujung URL resource: GET /.well-known/oauth-protected-resource/api/v1/external-mcp 200 GET /.well-known/oauth-protected-resource 200 same document GET /api/v1/external-mcp/.well-known/oauth-protected-resource 404 {"resource": "https://api.osfoundry.io/api/v1/external-mcp", "authorization servers": "https://api.osfoundry.io" , "scopes supported": "osfoundry:read", "osfoundry:write", "osfoundry:run", "osfoundry:models" } Kalau kamu membuat client sendiri, 404 itu adalah kesalahan pertama yang akan kamu buat. Ambil URL dari header, jangan menyusunnya sendiri. /.well-known/oauth-authorization-server RFC 8414 : {"issuer": "https://api.osfoundry.io", "authorization endpoint": "https://api.osfoundry.io/api/v1/external-oauth/authorize", "token endpoint": "https://api.osfoundry.io/api/v1/external-oauth/token", "registration endpoint": "https://api.osfoundry.io/api/v1/external-oauth/register", "grant types supported": "authorization code", "refresh token" , "token endpoint auth methods supported": "none" , "code challenge methods supported": "S256" , "authorization response iss parameter supported": true} Baca ini sebagai rangkaian keputusan. Ada registration endpoint , jadi host bisa mendaftarkan dirinya sendiri dynamic client registration dan pengguna tidak pernah berurusan dengan client ID. Autentikasi token-nya none , jadi host adalah public client tanpa client secret, dan PKCE dengan S256 yang mengikat code ke client yang memintanya. iss di authorization response mencegah salah tukar saat sebuah host berbicara dengan beberapa authorization server. Langkah authorize mengarahkan browser ke halaman consent osFoundry, https://osfoundry.io/external-assistant-authorize . Di sana pengguna memilih tepat satu workspace dan mencentang "I approve connecting this assistant to …" saya menyetujui asisten ini dihubungkan ke … . Persetujuan itu hanya menyebut satu workspace, tidak lebih. Koneksi baru belum punya project. Owner menambahkannya belakangan di aplikasi key icon → Connected → Add to project . Scope ditetapkan saat token diterbitkan. osfoundry:write menyatakan bahwa koneksi boleh melakukan aksi tulis. Scope itu tidak menyebut Note yang mana, dan tidak bisa tahu bahwa Note tersebut dipindahkan ke project lain sepuluh menit lalu. Catatan desain osFoundry mendaftar apa saja yang dibaca ulang di setiap panggilan: status koneksi, workspace, keanggotaan project, project dan ACL resource saat ini, keanggotaan channel atau role di session, tool-nya, dan scope OAuth. Orang yang menyetujui koneksi juga harus masih punya hak edit atas item tersebut. Penghapusan atau pemindahan "langsung berlaku". Alternatifnya adalah memasukkan daftar project ke dalam token, atau ke cache yang di-key oleh token, lalu memercayainya sampai kedaluwarsa. Itu lebih murah untuk ditulis. Rebuild di bawah mengimplementasikan keduanya supaya perbedaan perilakunya bisa diukur. Tidak ada binary Claude yang terlibat. client.py berperan sebagai host dan server.py sebagai server MCP. Keduanya hanya memakai standard library, sekitar 280 baris jika digabung, dan dijalankan di Python 3.9.6 pada Apple M1. Server-nya mengimplementasikan initialize , tools/list , dan tools/call lewat JSON-RPC di atas HTTP biasa, dua dokumen well-known, /authorize halaman consent otomatis disetujui untuk satu workspace , dan /token dengan PKCE S256. Beberapa route /admin/ menggantikan klik owner di tab Connected. Pengecekan yang berjalan sebelum setiap tool: python def access token info, tool, note id : if TOOL SCOPE tool not in token info "scopes" : return "insufficient scope" if CHECK == "token only": design B: trust issue-time state ok = note id is None or NOTES note id "project" in token info "projects at issue" return None if ok else "not in project" conn = CONNECTIONS.get token info "connection" if not conn or conn "status" = "active": return "connection revoked" if note id is None: return None note = NOTES.get note id if not note or note "workspace" = conn "workspace" : return "not found" if token info "connection" , note "project" not in PROJECT MEMBERS: return "not in project" return None Client mulai tanpa token. Ia memanggil sebuah tool, menerima 401, mem-parse resource metadata dari WWW-Authenticate , mengambil kedua dokumen metadata, menjalankan authorize dan token dengan PKCE verifier baru, lalu menjalankan skenarionya: bash $ CHECK=per call python3 server.py & python3 client.py 8767 discovery: first call - 401, metadata /.well-known/oauth-protected-resource/mcp, 5 round trips, 11.3 ms 1 fresh connection, no project yet 200 OK 2 after owner adds project A 200 OK "note-a1" 3 read note in A 200 OK "Q3 launch checklist" 4 read note in B never added 200 DENY not in project 5 write with a read-only token 403 DENY insufficient scope 6 write with read+write token 200 OK "saved" 7 same read after note moved to B 200 DENY not in project 8 same read after owner removes project A 200 DENY not in project 9 same read after revoke 200 DENY connection revoked Client yang sama terhadap CHECK=token only . Di sini token membawa daftar project dari refresh terakhirnya: 7 same read after note moved to B 200 DENY not in project 8 same read after owner removes project A 200 OK "Q3 launch checklist v2" 9 same read after revoke 200 OK "Q3 launch checklist v2" Langkah 1–7 identik. Langkah 7 tetap ditolak di desain token-only karena project milik note dicek secara live. Desain token-only hanya jadi basi untuk fakta yang disimpan di dalam token. Langkah 8 dan 9 adalah yang penting bagi owner: mereka sudah menghapus project, lalu mencabut koneksi, tapi chat tetap bisa membaca. Di desain token-only, kondisi itu bertahan sampai token kedaluwarsa, yaitu expires in: 3600 di rebuild ini. Penolakan terbagi di dua lapisan. Scope yang kurang adalah kegagalan di level protokol: 403 dengan error="insufficient scope" dan scope yang dibutuhkan di WWW-Authenticate , sehingga host bisa meminta lebih. Semua yang berkaitan dengan item mana yang terlibat adalah hasil tool biasa dengan isError: true . Otorisasi ulang tidak akan memperbaikinya, jadi tidak ada gunanya menyuruh host mencoba lagi. Diukur dengan time.perf counter ns di sekitar access di dalam server, 1,000 panggilan per run 500 diizinkan, 500 ditolak , di Mac yang juga sedang menjalankan job lain: | | median access | p99 access | median tool call loopback | |---|---|---|---| | cek per panggilan, 6 run | 1.3–1.6 µs | 6.1–7.0 µs | 0.43–0.78 ms | | token-only, 3 run | 0.7–1.6 µs | 3.5–7.7 µs | 0.35–1.36 ms 2 run | Di memori, pengecekan ulang tenggelam di noise panggilan HTTP loopback. Server sungguhan melakukan query database, bukan sekadar cek keanggotaan set, jadi biayanya lebih tinggi. Rebuild ini tidak bisa mengukurnya, jadi tidak ada angka yang diberikan. Sebagai gambaran, perjalanan jaringan ke endpoint live dari Tokyo sekitar 500 ms sampai 800 ms per request dengan urllib dan koneksi TLS baru setiap kali masing-masing n=10: median initialize 790 ms, tools/list 816 ms, tools/call yang 401 565 ms, tiap dokumen metadata sekitar 520–530 ms . Waktu sebuah connector tidak habis di pengecekan akses. Discovery-nya sendiri butuh 5 round trip HTTP secara lokal: 401, resource metadata, server metadata, authorize, token. Itu memakan 7.5–13.4 ms dalam tujuh run dan 34 ms sekali saat Mac sedang sibuk. Terhadap server live, tiga leg GET/POST yang bisa dilakukan tanpa kredensial totalnya kira-kira 1.6 s dari cold start. Leg authorize toh menunggu manusia. Di setup ini, host-lah yang memanggil. Host membuka koneksi, mengambil daftar tool, dan memutuskan kapan memanggil salah satunya. Server menjawab. Tidak ada apa pun di setup ini yang memberi server akses ke percakapan milik host. Rencana osFoundry menuliskannya di tabel kapabilitasnya: membuka atau mendaftar chat pribadi ChatGPT/Claude sembarang adalah "No", dengan keterangan "No general host API is provided for this". Hal itu membentuk tool-nya. Dengan link current chat , chat itu sendiri yang membuat link dari dalam chat. Label-nya dilaporkan sendiri oleh chat, dan osFoundry memakainya untuk routing, bukan sebagai identitas. Pekerjaan yang ditugaskan ke chat Claude yang ter-link menunggu sampai pengguna membuka chat itu dan memintanya untuk mengecek. ChatGPT punya MCP Events untuk chat yang berlangganan, dan rencana osFoundry memperlakukannya sebagai fitur khusus host tersebut. Sisi server memegang semua keputusan soal data, karena hanya sisi itu yang tahu kondisi terkini sebuah project. Sisi host memegang semua keputusan soal kapan bertindak dan apakah perlu bertanya ke pengguna. Pengecekan per panggilan inilah yang membuat server tetap benar ketika kedua sisi itu tidak lagi sinkron. resource metadata di WWW-Authenticate milik respons 401 dan sajikan RFC 9728 di /.well-known/oauth-protected-resource/