कोई client जानना चाहे कि कुछ बदला है या नहीं, तो osFoundry का MCP server उसे बस इतना ही देता है। यह इसी Mac से, बिना token के चलाया गया:
$ 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 Push के तीनों रास्ते बंद हैं। initialize सिर्फ़ tools declare करता है, और कुछ नहीं, वह भी listChanged: false के साथ, यानी tool list भी किसी बदलाव की खबर नहीं देगी। resources/subscribe पर "Method not found" आता है। MCP URL पर GET, जहाँ कोई Streamable HTTP server, server से client की तरफ़ stream खोलता, 405 लौटाता है। बचता है सिर्फ़ एक read-only tool:
{"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"]}]} "One granted" resource, "resume from an event sequence"। यह पूरा post इन्हीं शब्दों के बारे में है, और client side को Python में दोबारा बनाया गया है। यहाँ कोई codex binary install नहीं है, इसलिए client का रोल script निभाता है। osFoundry का server code public नहीं है: Part 1 और 2 live server हैं, और Part 3 एक rebuild है जिसके response shapes और activity rates उसके अपने हैं।
Codex के लिए push के बजाय poll ही क्यों?
Codex CLI कोई daemon नहीं है। जब आप उसे चलाते हैं तभी वह चलता है, एक turn पूरा करता है, और फिर बंद हो जाता है या आपका इंतज़ार करता है। Push channel के लिए आपके sessions के बीच कुछ न कुछ सुनते रहना चाहिए, और laptop पर अक्सर ऐसा कुछ होता ही नहीं। Sequence cursor इस तरीके से मेल खाता है: client हर resource के लिए एक integer रखता है, जब चाहे वापस आता है, और पूछता है कि उसके बाद क्या हुआ।
ऊपर के milliseconds Japan से Apple M1 पर नापे गए हैं, इसलिए वे server से ज़्यादा network route के बारे में बताते हैं। ज़्यादा अहम है rate-limit header। Unauthenticated responses में X-RateLimit-Limit: 2000 आता है, और reset लगभग 61 s बाद होता है। Signed-in client को भी यही budget मिलता है या नहीं, यह check नहीं किया गया, लेकिन 30 resources को हर 30 s पर poll करना भी सिर्फ़ 60 requests प्रति मिनट है। Codex वाले watcher को server load नहीं रोकता, उसके अपने turns रोकते हैं: हर poll एक tool call है जिसे model को करना और पढ़ना पड़ता है।
Client को कैसे पता चलता है कि वह क्या poll कर सकता है?
दो calls से, और schemas design के बारे में काफ़ी कुछ बता देते हैं:
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 कोई argument नहीं लेता, और schema नया argument जोड़ने का रास्ता भी बंद कर देता है:
list_granted_resources no arguments ok list_granted_resources project_id passed REJECT additionalProperties यहाँ server client को बताता है कि उसके पास क्या है, client कुछ माँगता नहीं। initialize के instructions की शुरुआत ही "Use list_granted_resources first." से होती है। जवाब में वे projects आते हैं जो connection में add किए गए हैं, और वे resources जो अलग से grant किए गए हैं। Web client (ExternalAssistantsPanel.tsx) में ये Projects section और "Older individual access" हैं।
list_project_resources को resource_type चाहिए। "इस project में जो कुछ भी है" वाली कोई listing नहीं है, इसलिए पूरी inventory के लिए हर type पर एक call लगती है, नौ types, और किसी type में 50 से ज़्यादा items हों तो उससे भी ज़्यादा (limit maximum 50, offset से paging)। इसका description project access का इकलौता exception भी जोड़ता है: "Ordinary Message channels require a separate invitation."
graph_session poll हो सकता है, पर list नहीं होता। Graphs owner के desktop पर रहते हैं और list_graph_sessions से आते हैं, वह भी तभी जब owner उन्हें desktop app से share करे।
Cursor client का क्यों है, server का क्यों नहीं?
osFoundry में दोनों तरह के हैं। Agent network पर wait_for_messages आपके लिए cursor रखता है: "Without after_sequence it resumes where your last call stopped", और यह timeout_s के साथ 50 तक long-poll करता है। poll_resource_events में इनमें से कुछ भी नहीं है। timeout_s reject हो जाता है, और description में सिर्फ़ "resume from" लिखा है, इसलिए number client को ही लाना होता है:
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 Server पर रखा cursor एक listener के लिए आसान है। जैसे ही दो clients एक connection share करते हैं, वह टूट जाता है, और ठीक यही होता है जब आप एक ही login से दो terminals में Codex खोलते हैं। Rebuild 600 channel events ऐसे दो terminals के सामने से गुज़ारता है जो random तरीके से बारी-बारी poll करते हैं:
two terminals, client cursor: A saw 600/600, B saw 600/600 two terminals, server cursor: A saw 305/600, B saw 295/600 हर connection के लिए server-side cursor हो तो हर terminal दूसरे का cursor आगे बढ़ा देता है, और दोनों को लगभग आधे events ही दिखते हैं। Request में after_sequence हो तो दोनों को सब कुछ दिखता है, और server पर किसी reader की state रखनी ही नहीं पड़ती। जिस resource log को कई tools अलग-अलग पढ़ सकते हैं, उसके cursor का सही मालिक client ही है।
Schema after_sequence को required नहीं बनाता, और यह भी नहीं बताता कि उसे छोड़ देने का क्या मतलब है। इस पर भरोसा न करें। Number save करें और हर बार भेजें।
एक पूरा page कितनी calls का पड़ता है?
limit 100 पर रुक जाता है। Bulk import के बाद किसी note पर सैकड़ों events इंतज़ार कर रहे हो सकते हैं। उन्हें खाली करने का मतलब है आख़िरी sequence से फिर call करना, जब तक कोई page अधूरा न लौटे:
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 100 माँगें। Rebuild में response का size खाली page के लिए 17 tokens और एक event के लिए 41 tokens है, cl100k_base से गिना गया। ये rebuild के shapes हैं, osFoundry के नहीं, लेकिन order of magnitude बात साफ़ कर देता है: खाली poll पढ़ना सस्ता है, इसलिए polling की असली लागत ज़्यादातर turns की गिनती में है।
Codex को 30 resources कितनी बार poll करने चाहिए?
Rebuild एक project को नौ listable types के 30 resources देता है, जिनकी activity बराबर नहीं है: 3 busy (हर मिनट एक event), 7 warm (हर 10 मिनट में एक), 20 cold (हर 2 घंटे में एक)। ये rates एक अनुमान हैं। यह simulated clock पर पाँच seeds के साथ 8 घंटे का दिन चलाता है:
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 हर 30 s पर fixed polling तेज़ है, लेकिन 95% बेकार: घंटे में 3,600 calls, लगभग सब खाली। हर 5 मिनट पर fixed polling सस्ती है, पर हर जगह धीमी, busy channel पर भी। Per-resource backoff, जो हर खाली poll के बाद किसी resource का interval दोगुना करता है और पहला event आते ही reset कर देता है, calls की गिनती में सस्ते schedule के करीब पहुँच जाता है और median delay में तेज़ वाले के करीब रहता है। वजह यह है कि busy resources 30 s पर बने रहते हैं और cold resources धीरे-धीरे दूर खिसक जाते हैं। कीमत cap है: 30 मिनट के cap पर किसी cold resource का पहला बदलाव आधा घंटा इंतज़ार कर सकता है।
Codex के लिए यह table असल में timer चलाने के ख़िलाफ़ ही दलील है। जब कोई पूछता है "क्या बदला?", तो saved cursors के साथ एक pass हर watched resource पर एक call लेता है, और उस पल तक की हर चीज़ का जवाब बिना किसी delay के देता है। Backoff schedule उस script के लिए है जो Codex को, या किसी और चीज़ को, लगातार निगरानी पर रखती है।
Owner access हटा दे तो क्या होता है?
Owner 4 h 07 min पर project हटा देता है। अब सभी 30 cursors ऐसे resources की तरफ़ इशारा करते हैं जिन्हें connection पढ़ नहीं सकता। सवाल यह है कि client उन्हें कब तक poll करता रहता है:
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 Timer पर grants check करने से या तो calls ख़र्च होती हैं या हज़ारों failed polls बर्बाद होते हैं। पहले refused poll को "grants दोबारा पढ़ो" का signal मानने की कीमत है एक failure और एक extra list_granted_resources। इसके बाद हर वह cursor हटा दें जिसका resource अब list में नहीं है। अगर owner बाद में project वापस जोड़ दे, तो client उन resources को नए सिरे से शुरू करता है।
Schemas client को कहाँ रोकते हैं?
बाकी cases, live inputSchemas के against jsonschema (Draft 2020-12) से check किए गए:
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" वही है जो लोग सबसे पहले try करते हैं। Project log वाला resource नहीं है। वह grant है जिससे resources दिखाई देते हैं।
पूरी script को M1 पर 20.2 s wall-clock लगे, 40 network calls मिलाकर। User CPU time 0.91 s रहा।
अगर आप इसे ख़ुद wire करें
- हर resource का एक log रखें और cursor client को रखने दें। तब एक connection पर दो readers एक-दूसरे के events नहीं छीन सकते, और server पर reader की कोई state नहीं रहती।
- Grant list को उसका अपना बिना-argument वाला tool दें, और client की शुरुआत वहीं से कराएँ। Discovery सिर्फ़ वही लौटाए जो owner ने grant किया है।
- Page size पर cap लगाएँ और बताएँ कि drain कैसे करना है (आख़िरी sequence से फिर call करें, जब तक page अधूरा न आए)।
- Backoff हर resource पर अलग रखें, global नहीं। Busy resources तेज़ रहते हैं, cold resources सस्ते हो जाते हैं।
- पहले refused poll पर grants दोबारा पढ़ें और जो cursors अब नहीं रहे उन्हें retry करने के बजाय हटा दें।
- Events पर action लेने के बाद ही cursor वापस लिखें, ताकि crash होने पर events skip होने के बजाय दोबारा आएँ।
osFoundry हर connection को कौन से tools देता है, यह tool scope page पर है। Protocol की छोटी परिभाषा MCP glossary entry में है।


