Kenapa osFoundry membuat Codex CLI melakukan polling cursor per resource alih-alih mengirim perubahan lewat push? A developer's hands-on test of the osFoundry MCP server found that all three push paths are closed: initialize declares only tools with listChanged false, resources/subscribe returns "Method not found," and a GET to the MCP URL returns HTTP 405. The only remaining option is a read-only poll_resource_events tool that resumes from an event sequence per granted resource, which the developer argues suits non-daemon clients like Codex CLI that store a cursor integer and ask what changed after it. Inilah semua yang ditawarkan server MCP osFoundry https://osfoundry.io/id kepada client yang ingin tahu kapan sesuatu berubah. Dijalankan dari Mac ini tanpa token: bash $ uv run --with tiktoken --with jsonschema python3 watch sim.py == Part 1: live wire, 8 calls each, ms == initialize HTTP 200 median 414.7 p90 757.8 tools/list HTTP 200 median 606.8 p90 716.7 resources/subscribe HTTP 200 median 450.6 p90 550.2 GET stream HTTP 405 median 423.9 p90 475.4 tools/call poll no token HTTP 401 median 322.0 p90 463.9 initialize capabilities: {"tools": {"listChanged": false}} resources/subscribe - {"code": -32601, "message": "Method not found"} GET body - '' poll without token - isError True | Authentication required rate limit headers: limit 2000, reset in 61 s Ketiga jalur push tertutup. initialize hanya mendeklarasikan tools, tidak ada yang lain, dengan listChanged: false , jadi daftar tools pun tidak akan mengumumkan perubahan. resources/subscribe mengembalikan "Method not found". GET ke URL MCP, tempat server Streamable HTTP biasanya membuka stream dari server ke client, mengembalikan 405. Yang tersisa hanya satu tool read-only: {"name": "poll resource events", "description": "Resume from an event sequence for one granted channel, Note, knowledge base, record, or session.", "inputSchema": {"properties": { "resource type": {"enum": "message channel", "note", "knowledge base", "record channel", "room", "gitnest repository", "agent session", "team session", "loop session", "graph session" }, "resource id": {"type": "string"}, "after sequence": {"type": "integer", "minimum": 0}, "limit": {"type": "integer", "minimum": 1, "maximum": 100}}, "required": "resource type", "resource id" , "additionalProperties": false}, "annotations": {"readOnlyHint": true, "openWorldHint": false, "destructiveHint": false}, "securitySchemes": {"type": "oauth2", "scopes": "osfoundry:read" } } Resource "one granted", "resume from an event sequence". Sisa tulisan ini membahas kata-kata itu, dengan sisi client yang dibangun ulang di Python. Di mesin ini tidak ada binary codex, jadi script-nya yang berperan sebagai client. Kode server osFoundry tidak publik: Part 1 dan 2 adalah server live, sedangkan Part 3 adalah rekonstruksi dengan bentuk respons dan tingkat aktivitasnya sendiri. Codex CLI bukan daemon. Ia jalan saat Anda menjalankannya, mengerjakan satu turn, lalu keluar atau menunggu Anda. Channel push butuh sesuatu yang terus mendengarkan di antara sesi Anda, dan di laptop sering kali tidak ada apa-apa. Cursor sequence cocok dengan pola itu: client menyimpan satu integer per resource, kembali kapan saja, dan menanyakan apa yang terjadi setelah angka itu. Angka milidetik di atas diukur dari Jepang di Apple M1, jadi lebih banyak menggambarkan rute jaringan daripada server. Header rate limit lebih penting. Respons tanpa autentikasi membawa X-RateLimit-Limit: 2000 dengan reset sekitar 61 s ke depan. Belum dicek apakah client yang sudah login mendapat jatah yang sama, tetapi polling 30 resource setiap 30 s pun hanya 60 request per menit. Yang membatasi watcher berbasis Codex bukan beban server, melainkan turn-nya sendiri: setiap poll adalah tool call yang harus dibuat dan dibaca oleh model. Dengan dua call, dan schema-nya banyak bercerita tentang desainnya: list granted resources osfoundry:read ro=True required= props= list project resources osfoundry:read ro=True required= 'project id', 'resource type' props= 'project id', 'resource type', 'search', 'offset', 'limit' poll resource events osfoundry:read ro=True required= 'resource type', 'resource id' props= 'resource type', 'resource id', 'after sequence', 'limit' 59 tools on server; these three = 591 of 12662 tokens 4.7%, cl100k base poll resource events types: 10 | list project resources types: 9 | only pollable: 'graph session' list granted resources sama sekali tidak menerima argumen, dan schema-nya menutup kemungkinan menambahkannya: list granted resources no arguments ok list granted resources project id passed REJECT additionalProperties Di sini server yang memberi tahu client apa yang dimilikinya, bukan client yang meminta sesuatu. Instruksi initialize diawali dengan "Use list granted resources first." Jawabannya berisi project yang ditambahkan ke koneksi dan resource yang diberikan secara individual. Di web client ExternalAssistantsPanel.tsx , keduanya adalah bagian Projects dan "Older individual access". list project resources membutuhkan resource type . Tidak ada listing "semua isi project ini", jadi inventaris lengkap berarti satu call per tipe, sembilan tipe, dan lebih banyak lagi kalau satu tipe punya lebih dari 50 item limit maksimum 50, paging dengan offset . Deskripsinya menambahkan satu-satunya pengecualian untuk akses project: "Ordinary Message channels require a separate invitation." graph session bisa di-poll tetapi tidak bisa di-list. Graph tinggal di desktop owner dan masuk lewat list graph sessions , itu pun hanya setelah owner membagikannya dari aplikasi desktop. osFoundry punya keduanya. wait for messages di jaringan agent menyimpan cursor untuk Anda: "Without after sequence it resumes where your last call stopped", dan ia melakukan long polling dengan timeout s sampai 50. poll resource events tidak punya keduanya. timeout s ditolak, dan deskripsinya hanya bilang "resume from", jadi client yang membawa angkanya: wait for messages props: 'after sequence', 'timeout s', 'limit' timeout s max 50 | poll resource events has timeout: False poll resource events timeout s 30 REJECT additionalProperties Cursor di sisi server nyaman untuk satu listener. Ia rusak saat dua client berbagi satu koneksi, dan itulah yang terjadi saat Anda membuka Codex di dua terminal dengan login yang sama. Rekonstruksi ini mengalirkan 600 event channel ke dua terminal yang bergantian melakukan poll secara acak: two terminals, client cursor: A saw 600/600, B saw 600/600 two terminals, server cursor: A saw 305/600, B saw 295/600 Dengan satu cursor per koneksi di server, setiap terminal memajukan cursor milik terminal lain, dan masing-masing hanya melihat sekitar separuh. Dengan after sequence di request, keduanya melihat semuanya, dan server sama sekali tidak menyimpan state per pembaca. Untuk log resource yang bisa dibaca beberapa tool secara terpisah, client adalah pemilik cursor yang tepat. Schema tidak mewajibkan after sequence , dan juga tidak menjelaskan apa artinya kalau dihilangkan. Jangan mengandalkannya. Simpan angkanya dan selalu kirim. limit maksimal 100. Setelah bulk import, sebuah note bisa punya ratusan event yang menunggu. Untuk menghabiskannya, panggil lagi dari sequence terakhir sampai halaman yang kembali tidak penuh: burst of 350 events, limit 100: 4 calls to drain, cursor ends at 350 burst of 350 events, limit 50: 7 calls to drain, cursor ends at 350 burst of 350 events, limit 20: 18 calls to drain, cursor ends at 350 Minta 100. Di rekonstruksi, ukuran respons adalah 17 token untuk halaman kosong dan 41 untuk satu event, dihitung dengan cl100k base . Itu bentuk respons rekonstruksi, bukan milik osFoundry, tetapi orde besarnya sudah cukup menjelaskan: poll kosong murah untuk dibaca, jadi biaya polling sebagian besar ada di jumlah turn. Rekonstruksi ini memberi satu project 30 resource dari sembilan tipe yang bisa di-list, dengan aktivitas yang timpang: 3 sibuk satu event per menit , 7 sedang satu setiap 10 menit , 20 sepi satu setiap 2 jam . Angka-angka ini asumsi. Simulasinya menjalankan hari kerja 8 jam dengan lima seed, di jam simulasi: every 30 s, all 30 calls/hour 3600 empty 94.7% seen late median 14.9 s p90 27.0 s max 30 s 1843 events/day every 5 min, all 30 calls/hour 360 empty 78.0% seen late median 150.9 s p90 271.1 s max 300 s 1843 events/day 30 s, x2 when empty, cap 10 min calls/hour 502 empty 70.2% seen late median 34.9 s p90 206.4 s max 599 s 1843 events/day 30 s, x2 when empty, cap 30 min calls/hour 420 empty 65.3% seen late median 35.6 s p90 238.4 s max 1799 s 1843 events/day server window for comparison: 2000 requests per ~61 s Tetap setiap 30 s itu cepat tetapi 95% terbuang: 3,600 call per jam, hampir semuanya kosong. Tetap setiap 5 menit itu murah tetapi lambat di mana-mana, termasuk di channel yang sibuk. Backoff per resource, yang menggandakan interval sebuah resource setelah setiap poll kosong dan me-reset-nya saat event pertama datang, mendekati jumlah call jadwal murah dengan median keterlambatan mendekati jadwal cepat. Penyebabnya, resource sibuk tetap di 30 s sementara resource sepi makin jarang di-poll. Harganya ada di batas atas: dengan 30 menit, perubahan pertama di resource sepi bisa menunggu setengah jam. Untuk Codex, tabel ini sebenarnya lebih menjadi alasan untuk tidak memakai timer sama sekali. Saat seseorang bertanya "apa yang berubah?", satu putaran dengan cursor tersimpan hanya butuh satu call per resource yang dipantau, dan menjawab tanpa keterlambatan untuk semua yang terjadi sampai saat itu. Jadwal backoff ditujukan untuk script yang membuat Codex, atau apa pun, terus memantau. Owner menghapus project di 4 h 07 min. Ke-30 cursor sekarang menunjuk ke resource yang tidak bisa dibaca oleh koneksi. Pertanyaannya, berapa lama client terus melakukan polling ke sana: owner removes the project at 4 h 07 min 30 resources lose access : re-list grants every 10 min failed polls after removal 180, list granted resources calls 25 re-list grants every 60 min failed polls after removal 3180, list granted resources calls 5 re-list grants on first error failed polls after removal 1, list granted resources calls 5 Mengecek grant dengan timer berarti antara menghabiskan call atau membuang ribuan poll gagal. Memperlakukan poll pertama yang ditolak sebagai sinyal "baca ulang grant" hanya memakan satu kegagalan dan satu list granted resources tambahan. Setelah itu, buang setiap cursor yang resource-nya sudah tidak ada di daftar. Kalau owner menambahkan project itu lagi nanti, client memulai resource tersebut dari awal. Kasus sisanya, dicek dengan jsonschema Draft 2020-12 terhadap inputSchema live: list project resources no resource type REJECT required list project resources limit 50 ok list project resources limit 51 REJECT maximum: limit list project resources graph session REJECT enum: resource type list project resources search 101 chars REJECT maxLength: search poll resource events no after sequence ok poll resource events graph session ok poll resource events after sequence -1 REJECT minimum: after sequence poll resource events limit 100 ok poll resource events limit 101 REJECT maximum: limit poll resource events resource type project REJECT enum: resource type resource type: "project" adalah yang paling sering dicoba pertama kali. Project bukan resource yang punya log. Project adalah grant yang membuat resource terlihat. Seluruh script memakan 20.2 s waktu nyata di M1, termasuk 40 network call. Waktu CPU user-nya 0.91 s. Cara osFoundry menentukan tools yang didapat setiap koneksi ada di halaman tool scope https://osfoundry.io/id/platform/tool-scope . Definisi singkat protokolnya ada di entri glosarium MCP https://osfoundry.io/id/glossary/mcp .