{"slug": "bagaimana-custom-connector-claude-menemukan-server-oauth-osfoundry-dan-kenapa-di", "title": "Bagaimana custom connector Claude menemukan server OAuth osFoundry, dan kenapa akses dicek di setiap panggilan?", "summary": "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.", "body_md": "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:\n\n``` bash\n$ curl -s -D - -X POST https://api.osfoundry.io/api/v1/external-mcp \\\n    -H 'content-type: application/json' \\\n    -H 'accept: application/json, text/event-stream' \\\n    -d '{\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"tools/call\",\n         \"params\":{\"name\":\"list_project_resources\",\"arguments\":{}}}'\n\nHTTP/2 401\nwww-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\"\n\n{\"jsonrpc\":\"2.0\",\"id\":3,\"result\":{\"isError\":true,\n  \"content\":[{\"type\":\"text\",\"text\":\"Authentication required\"}],\n  \"_meta\":{\"mcp/www_authenticate\":[\"Bearer resource_metadata=\\\"https://api.osfoundry.io/.well-known/oauth-protected-resource/api/v1/external-mcp\\\", ...\"]}}}\n```\n\nSatu 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.\n\nTiga panggilan, tiga jawaban berbeda:\n\n| Request | Status | Yang dikembalikan | \n|---|---|---|\n| `initialize` | 200 | protokol `2025-06-18` , server`osfoundry 0.1.0` , plus instruksi | \n| `tools/list` | 200 | 59 tool, 45,368 byte | \n| `tools/call` | 401 | `WWW-Authenticate` yang menunjuk ke resource metadata | \n| `GET` ke endpoint | 405 | tidak ada stream SSE lewat GET | \n\nHandshake 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:\n\n```\nUse list_granted_resources first. Every Note edit must use the exact tab ID.\nLocal-only data is unavailable to cloud clients.\n```\n\nSetiap tool di `tools/list` mendeklarasikan scope OAuth yang dibutuhkannya dan apakah tool itu hanya membaca:\n\n```\n{\"name\": \"edit_record_item\",\n \"annotations\": {\"readOnlyHint\": false, \"destructiveHint\": true, \"openWorldHint\": false},\n \"securitySchemes\": [{\"type\": \"oauth2\", \"scopes\": [\"osfoundry:write\"]}]}\n```\n\nDari 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.\n\n`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:\n\n```\nGET /.well-known/oauth-protected-resource/api/v1/external-mcp   200\nGET /.well-known/oauth-protected-resource                       200 (same document)\nGET /api/v1/external-mcp/.well-known/oauth-protected-resource   404\n{\"resource\": \"https://api.osfoundry.io/api/v1/external-mcp\",\n \"authorization_servers\": [\"https://api.osfoundry.io\"],\n \"scopes_supported\": [\"osfoundry:read\", \"osfoundry:write\", \"osfoundry:run\", \"osfoundry:models\"]}\n```\n\nKalau kamu membuat client sendiri, 404 itu adalah kesalahan pertama yang akan kamu buat. Ambil URL dari header, jangan menyusunnya sendiri.\n\n`/.well-known/oauth-authorization-server` (RFC 8414):\n\n```\n{\"issuer\": \"https://api.osfoundry.io\",\n \"authorization_endpoint\": \"https://api.osfoundry.io/api/v1/external-oauth/authorize\",\n \"token_endpoint\": \"https://api.osfoundry.io/api/v1/external-oauth/token\",\n \"registration_endpoint\": \"https://api.osfoundry.io/api/v1/external-oauth/register\",\n \"grant_types_supported\": [\"authorization_code\", \"refresh_token\"],\n \"token_endpoint_auth_methods_supported\": [\"none\"],\n \"code_challenge_methods_supported\": [\"S256\"],\n \"authorization_response_iss_parameter_supported\": true}\n```\n\nBaca 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.\n\nLangkah 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).\n\nScope 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\".\n\nAlternatifnya 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.\n\nTidak 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.\n\nPengecekan yang berjalan sebelum setiap tool:\n\n``` python\ndef access(token_info, tool, note_id):\n    if TOOL_SCOPE[tool] not in token_info[\"scopes\"]:\n        return \"insufficient_scope\"\n    if CHECK == \"token_only\":                     # design B: trust issue-time state\n        ok = note_id is None or NOTES[note_id][\"project\"] in token_info[\"projects_at_issue\"]\n        return None if ok else \"not_in_project\"\n    conn = CONNECTIONS.get(token_info[\"connection\"])\n    if not conn or conn[\"status\"] != \"active\":\n        return \"connection_revoked\"\n    if note_id is None:\n        return None\n    note = NOTES.get(note_id)\n    if not note or note[\"workspace\"] != conn[\"workspace\"]:\n        return \"not_found\"\n    if (token_info[\"connection\"], note[\"project\"]) not in PROJECT_MEMBERS:\n        return \"not_in_project\"\n    return None\n```\n\nClient 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:\n\n``` bash\n$ CHECK=per_call python3 server.py &  python3 client.py 8767\ndiscovery: first call -> 401, metadata /.well-known/oauth-protected-resource/mcp, 5 round trips, 11.3 ms\n1 fresh connection, no project yet           200 OK   []\n2 after owner adds project A                 200 OK   [\"note-a1\"]\n3 read note in A                             200 OK   \"Q3 launch checklist\"\n4 read note in B (never added)               200 DENY not_in_project\n5 write with a read-only token               403 DENY insufficient_scope\n6 write with read+write token                200 OK   \"saved\"\n7 same read after note moved to B            200 DENY not_in_project\n8 same read after owner removes project A    200 DENY not_in_project\n9 same read after revoke                     200 DENY connection_revoked\n```\n\nClient yang sama terhadap `CHECK=token_only`. Di sini token membawa daftar project dari refresh terakhirnya:\n\n```\n7 same read after note moved to B            200 DENY not_in_project\n8 same read after owner removes project A    200 OK   \"Q3 launch checklist v2\"\n9 same read after revoke                     200 OK   \"Q3 launch checklist v2\"\n```\n\nLangkah 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.\n\nPenolakan 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.\n\nDiukur 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:\n\n|  | median access() | p99 access() | median tool call loopback | \n|---|---|---|---|\n| cek per panggilan, 6 run | 1.3–1.6 µs | 6.1–7.0 µs | 0.43–0.78 ms | \n| token-only, 3 run | 0.7–1.6 µs | 3.5–7.7 µs | 0.35–1.36 ms (2 run) | \n\nDi 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.\n\nDiscovery-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.\n\nDi 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\".\n\nHal 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.\n\nSisi 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.\n\n`resource_metadata` di `WWW-Authenticate` milik respons 401 dan sajikan RFC 9728 di `/.well-known/oauth-protected-resource/<resource path>`. Client yang menyusun URL sendiri bisa salah. Header itu menyelamatkan setiap client dari kesalahan tersebut.`initialize` dan `tools/list` dan menjaga `tools/call`. Kalau deskripsi tool kamu membocorkan sesuatu tentang tenant, jaga juga `readOnlyHint` per tool. Host bisa membangun prompt persetujuan dari situ, dan 403 `insufficient_scope` memberi host sesuatu yang konkret untuk diminta ulang.`expected_version` dan idempotency key. Host melakukan retry, dan model mengulang panggilan.\nSisi MCP dari spesifikasinya ada di [halaman otorisasi MCP](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization). Definisi singkat versi osFoundry ada di [entri glosariumnya tentang MCP](https://osfoundry.io/id/glossary/mcp).", "url": "https://wpnews.pro/news/bagaimana-custom-connector-claude-menemukan-server-oauth-osfoundry-dan-kenapa-di", "canonical_source": "https://dev.to/bbt-id/bagaimana-custom-connector-claude-menemukan-server-oauth-osfoundry-dan-kenapa-akses-dicek-di-4084", "published_at": "2026-11-06 10:45:59+00:00", "updated_at": "2026-11-06 10:52:47.318750+00:00", "lang": "en", "topics": ["ai-agents", "agent-protocols", "ai-tools", "developer-tools"], "entities": ["Claude", "osFoundry", "ChatGPT", "Anthropic", "OpenAI"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/bagaimana-custom-connector-claude-menemukan-server-oauth-osfoundry-dan-kenapa-di", "markdown": "https://wpnews.pro/news/bagaimana-custom-connector-claude-menemukan-server-oauth-osfoundry-dan-kenapa-di.md", "text": "https://wpnews.pro/news/bagaimana-custom-connector-claude-menemukan-server-oauth-osfoundry-dan-kenapa-di.txt", "jsonld": "https://wpnews.pro/news/bagaimana-custom-connector-claude-menemukan-server-oauth-osfoundry-dan-kenapa-di.jsonld"}}