DEV Community

Cover image for Bagaimana custom connector Claude menemukan server OAuth osFoundry, dan kenapa akses dicek di setiap panggilan?
BBT for BBT Indonesia

Posted on

Bagaimana custom connector Claude menemukan server OAuth osFoundry, dan kenapa akses dicek di setiap panggilan?

Ini yang diterima client MCP remote saat memanggil tool osFoundry tanpa token. Request-nya dikirim dengan curl dari sebuah Mac di Tokyo pada 2026-11-06:

$ 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\", ..."]}}}
Enter fullscreen mode Exit fullscreen mode

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.

Apa yang dilihat client sebelum punya token?

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.
Enter fullscreen mode Exit fullscreen mode

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"]}]}
Enter fullscreen mode Exit fullscreen mode

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.

Ke mana 401 itu mengarahkannya?

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
Enter fullscreen mode Exit fullscreen mode
{"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"]}
Enter fullscreen mode Exit fullscreen mode

Kalau kamu membuat client sendiri, 404 itu adalah kesalahan pertama yang akan kamu buat. Ambil URL dari header, jangan menyusunnya sendiri.

Apa kata authorization server tentang dirinya?

/.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}
Enter fullscreen mode Exit fullscreen mode

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).

Kenapa scope saja tidak cukup?

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.

Rebuild-nya

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:

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
Enter fullscreen mode Exit fullscreen mode

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:

$ 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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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.

Berapa biaya pengecekan di setiap panggilan?

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.

Kenapa Claude bisa memanggil osFoundry, tapi osFoundry tidak bisa membuka chat Claude kamu?

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.

Kalau kamu memasangnya sendiri

  • Taruh 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.
  • Tentukan apa yang publik sebelum login. osFoundry membuka initialize dan tools/list dan menjaga tools/call. Kalau deskripsi tool kamu membocorkan sesuatu tentang tenant, jaga juga tools/list.
  • Deklarasikan scope dan readOnlyHint per tool. Host bisa membangun prompt persetujuan dari situ, dan 403 insufficient_scope memberi host sesuatu yang konkret untuk diminta ulang.
  • Buat token tetap kecil: siapa, koneksi mana, scope apa. Baca ulang workspace, keanggotaan project, project milik resource, dan role terkini orang yang menyetujui di setiap panggilan, supaya pencabutan dan "pindah ke project lain" langsung berlaku di panggilan berikutnya.
  • Kembalikan penolakan resource sebagai hasil tool, bukan sebagai 401/403. Token yang berbeda tidak akan memperbaiki "Note ini ada di project B".
  • Buat operasi tulis membawa expected_version dan idempotency key. Host melakukan retry, dan model mengulang panggilan.

Sisi MCP dari spesifikasinya ada di halaman otorisasi MCP. Definisi singkat versi osFoundry ada di entri glosariumnya tentang MCP.

Top comments (0)