Claude का custom connector osFoundry का OAuth server कैसे ढूंढता है, और हर call पर access क्यों check होता है?
जब कोई remote MCP client बिना token के osFoundry का कोई tool call करता है, तो उसे यह मिलता है। यह request 2026-11-06 को Tokyo में एक Mac से curl से भेजी गई थी: $ curl -s -D - -X POST https://api.osfoundry.io/api/v1/ex
जब कोई remote MCP client बिना token के osFoundry का कोई tool call करता है, तो उसे यह मिलता है। यह request 2026-11-06 को Tokyo में एक Mac से curl से भेजी गई थी:
$ 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\", ..."]}}}
Claude के custom connector को, या ChatGPT के developer mode वाले MCP connection को, sign-in page ढूंढने के लिए बस यही एक header काफ़ी है। User को सिर्फ़ URL paste करना होता है। नीचे जो कुछ भी है वह live endpoint पर बिना authentication वाली requests से लिया गया है, साथ में Python का एक छोटा rebuild है जो locally चलाया गया। यह rebuild वह हिस्सा मापता है जो curl नहीं दिखा सकता: sign-in के बाद access के साथ क्या होता है।
Token मिलने से पहले client को क्या दिखता है?
तीन calls, तीन अलग जवाब:
| Request | Status | क्या वापस आता है |
|---|---|---|
initialize |
200 | protocol 2025-06-18, server osfoundry 0.1.0, साथ में instructions |
tools/list |
200 | 59 tools, 45,368 bytes |
tools/call |
401 |
WWW-Authenticate, जो resource metadata की ओर इशारा करता है |
endpoint पर GET
|
405 | GET पर कोई SSE stream नहीं |
Handshake और tool catalogue public हैं। सिर्फ़ tool call करने के लिए token चाहिए। इससे host किसी के sign in करने से पहले ही user को दिखा सकता है कि कौन-कौन से tools मौजूद हैं। Catalogue में कोई user data नहीं है, और initialize में मौजूद instructions string generic है:
Use list_granted_resources first. Every Note edit must use the exact tab ID.
Local-only data is unavailable to cloud clients.
tools/list का हर tool बताता है कि उसे कौन-सा OAuth scope चाहिए और क्या वह सिर्फ़ read करता है:
{"name": "edit_record_item",
"annotations": {"readOnlyHint": false, "destructiveHint": true, "openWorldHint": false},
"securitySchemes": [{"type": "oauth2", "scopes": ["osfoundry:write"]}]}
59 tools में से: 30 को osfoundry:read चाहिए और वे readOnlyHint: true से marked हैं, 26 को osfoundry:write चाहिए, और 3 को osfoundry:run चाहिए (request_session_run, run_graph_session, cancel_graph_run)। Host readOnlyHint के आधार पर तय कर सकता है कि किन calls पर approval prompt दिखाना है।
401 उसे कहां भेजता है?
resource_metadata RFC 9728 (OAuth Protected Resource Metadata) को follow करता है। Well-known suffix host के ठीक बाद आता है, और resource path उसके पीछे जुड़ता है। इसे resource URL के आख़िर में नहीं जोड़ा जाता:
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"]}
अगर आप client हाथ से बना रहे हैं, तो सबसे पहली गलती आप यही 404 वाली करेंगे। URL ख़ुद बनाने की बजाय header से पढ़िए।
Authorization server अपने बारे में क्या बताता है?
/.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}
इसे फ़ैसलों की एक chain की तरह पढ़िए। इसमें registration_endpoint है, इसलिए host ख़ुद को register कर सकता है (dynamic client registration) और user को कभी client ID से जूझना नहीं पड़ता। Token auth none है, यानी host एक public client है जिसके पास client secret नहीं है, और S256 वाला PKCE ही code को उस client से बांधता है जिसने उसे मांगा था। Authorization response में iss उस गड़बड़ी से बचाता है जब कोई host कई authorization servers से बात कर रहा हो।
Authorize step browser को osFoundry के consent page https://osfoundry.io/external-assistant-authorize पर भेजता है। वहां user ठीक एक workspace चुनता है और "I approve connecting this assistant to …" (मैं इस assistant को … से जोड़ने की अनुमति देता हूं) पर tick करता है। Approval में सिर्फ़ एक workspace का नाम होता है, और कुछ नहीं। नए connection में कोई project नहीं होता। Owner उन्हें बाद में app में जोड़ता है (key icon → Connected → Add to project)।
सिर्फ़ scope काफ़ी क्यों नहीं है?
Scope उसी समय तय हो जाता है जब token mint होता है। osfoundry:write बताता है कि connection write वाले actions कर सकता है। यह नहीं बताता कि कौन-सा Note, और इसे यह पता भी नहीं हो सकता कि वह Note दस मिनट पहले किसी दूसरे project में चला गया। osFoundry के design notes में लिखा है कि हर call पर क्या दोबारा पढ़ा जाता है: connection status, workspace, project membership, resource का मौजूदा project और ACL, channel membership या session role, tool, और OAuth scope। जिस व्यक्ति ने connection approve किया था, उसके पास उस item पर अभी भी edit rights होने चाहिए। हटाना या reassign करना "तुरंत लागू होता है"।
दूसरा रास्ता यह है कि project list को token में डाल दिया जाए, या token को key बनाकर cache में रख दिया जाए, और expiry तक उस पर भरोसा किया जाए। यह लिखने में सस्ता है। नीचे वाला rebuild दोनों को implement करता है ताकि behaviour का फ़र्क़ मापा जा सके।
Rebuild
इसमें कोई Claude binary शामिल नहीं है। client.py host का रोल निभाता है और server.py MCP server का। दोनों सिर्फ़ standard library इस्तेमाल करते हैं, मिलाकर लगभग 280 lines, और Apple M1 पर Python 3.9.6 में चलाए गए। Server plain HTTP JSON-RPC पर initialize, tools/list और tools/call implement करता है, साथ में दोनों well-known documents, /authorize (consent page एक workspace के लिए अपने आप approve हो जाता है), और S256 PKCE के साथ /token। कुछ /admin/* routes Connected tab में owner के clicks की जगह लेते हैं।
हर tool से पहले चलने वाला check:
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 बिना token के शुरू होता है। वह एक tool call करता है, 401 लेता है, WWW-Authenticate से resource_metadata parse करता है, दोनों metadata documents fetch करता है, नए PKCE verifier के साथ authorize और token चलाता है, और फिर scenario से गुज़रता है:
$ 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 CHECK=token_only के ख़िलाफ़। यहां token में उसके पिछले refresh के समय की project list होती है:
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"
Steps 1–7 एक जैसे हैं। Token-only design में भी step 7 deny होता है, क्योंकि note का project live lookup किया जाता है। Token-only design सिर्फ़ उन्हीं बातों पर पुराना पड़ता है जो token के अंदर रखी हैं। Owner के लिए असली मायने steps 8 और 9 के हैं: उन्होंने project हटाया, फिर connection revoke किया, और chat फिर भी पढ़ती रही। Token-only design में यह token expire होने तक चलता है, जो इस rebuild में expires_in: 3600 है।
Denials दो layers में बंटे हैं। Scope न होना protocol level की failure है: error="insufficient_scope" के साथ 403, और ज़रूरी scope WWW-Authenticate में, ताकि host ज़्यादा scope मांग सके। कौन-सा item शामिल है, उससे जुड़ी हर चीज़ एक सामान्य tool result है जिसमें isError: true होता है। दोबारा authorise करने से वह ठीक नहीं होगा, इसलिए host को retry करने को कहने का कोई मतलब नहीं।
हर call पर check की क़ीमत क्या है?
Server के अंदर access() के चारों ओर time.perf_counter_ns() से मापा गया, हर run में 1,000 calls (500 allowed, 500 denied), एक ऐसे Mac पर जिस पर दूसरे jobs भी चल रहे थे:
| access() median | access() p99 | loopback tool call, median | |
|---|---|---|---|
| हर call पर check, 6 runs | 1.3–1.6 µs | 6.1–7.0 µs | 0.43–0.78 ms |
| सिर्फ़ token, 3 runs | 0.7–1.6 µs | 3.5–7.7 µs | 0.35–1.36 ms (2 runs) |
Memory में यह recheck loopback HTTP call के noise में खो जाता है। असली server set membership की जगह database lookups करता है, इसलिए उसकी क़ीमत ज़्यादा होती है। यह rebuild उसे माप नहीं सकता, इसलिए उसके लिए कोई number नहीं दिया गया है। अंदाज़े के लिए, Tokyo से live endpoint तक network leg urllib में हर बार नए TLS connection के साथ लगभग 500 ms से 800 ms प्रति request है (हर एक के लिए n=10: initialize median 790 ms, tools/list 816 ms, 401 वाला tools/call 565 ms, हर metadata document लगभग 520–530 ms)। Connector का समय access check में नहीं जाता।
Discovery ख़ुद locally 5 HTTP round trips थी: 401, resource metadata, server metadata, authorize, token। सात runs में इसमें 7.5–13.4 ms लगे, और एक बार Mac busy होने पर 34 ms। Live server के ख़िलाफ़, बिना credentials के किए जा सकने वाले तीन GET/POST legs cold start से मिलाकर लगभग 1.6 s लेते हैं। Authorize leg तो वैसे भी किसी इंसान का इंतज़ार करता है।
Claude osFoundry को call कर सकता है, पर osFoundry आपकी Claude chats क्यों नहीं खोल सकता?
इस setup में caller host है। वही connection खोलता है, tools list करता है और तय करता है कि कब कौन-सा tool call करना है। Server सिर्फ़ जवाब देता है। इस setup में ऐसा कुछ नहीं है जो server को host की conversations तक पहुंच दे। osFoundry का plan यह बात अपनी capability table में कहता है: मनचाही personal ChatGPT/Claude chats खोलना या उनकी list निकालना "No" है, साथ में "No general host API is provided for this"।
इसी से tools का design तय होता है। link_current_chat में chat ख़ुद अपने अंदर से link बनाती है। उसका label chat ख़ुद बताती है, और osFoundry उसे routing के लिए इस्तेमाल करता है, identity के तौर पर नहीं। किसी linked Claude chat को दिया गया काम तब तक इंतज़ार करता है जब तक user वह chat खोलकर उससे check करने को न कहे। ChatGPT में subscribed chats के लिए MCP Events हैं, और osFoundry का plan इसे host-specific मानता है।
Data से जुड़े सारे फ़ैसले server side के पास हैं, क्योंकि सिर्फ़ उसे ही किसी project की मौजूदा स्थिति पता होती है। कब action लेना है और user से पूछना है या नहीं, ये सारे फ़ैसले host side के पास हैं। हर call पर होने वाला check ही server को सही बनाए रखता है जब ये दोनों sides एक-दूसरे से अलग हो जाती हैं।
अगर आप इसे ख़ुद wire up करें
- 401 के
WWW-Authenticateमेंresource_metadataडालिए और RFC 9728 को/.well-known/oauth-protected-resource/<resource path>पर serve कीजिए। जो client URL हाथ से बनाता है, वह गलती कर सकता है। Header हर client को इससे बचा लेता है। - Sign-in से पहले क्या public है, यह तय कीजिए। osFoundry
initializeऔरtools/listखुला रखता है औरtools/callको guard करता है। अगर आपके tool descriptions tenants के बारे में कुछ भी leak करते हैं, तोtools/listको भी guard कीजिए। - हर tool के लिए एक scope और
readOnlyHintdeclare कीजिए। Hosts इनसे approval prompts बना सकते हैं, और 403insufficient_scopehost को दोबारा मांगने के लिए कुछ ठोस देता है। - Token छोटा रखिए: कौन, कौन-सा connection, कौन-से scopes। Workspace, project membership, resource का project और approve करने वाले का मौजूदा role हर call पर दोबारा पढ़िए, ताकि revoke और "दूसरे project में move करना" अगली ही call पर असर करें।
- Resource denials को 401/403 की बजाय tool results के रूप में लौटाइए। दूसरा token "यह Note project B में है" को ठीक नहीं करेगा।
- Writes के साथ
expected_versionऔर एक idempotency key भेजिए। Hosts retry करते हैं, और models calls दोबारा भेजते हैं।
Spec का MCP वाला हिस्सा MCP authorization page पर है। osFoundry की अपनी छोटी परिभाषा उसकी MCP वाली glossary entry में है।
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.