<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: BBT Français</title>
    <description>The latest articles on DEV Community by BBT Français (bbt-fr).</description>
    <link>https://dev.to/bbt-fr</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F15899%2F0f2dfaa2-407e-410a-a2f7-18bbab6fe435.png</url>
      <title>DEV Community: BBT Français</title>
      <link>https://dev.to/bbt-fr</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bbt-fr"/>
    <language>en</language>
    <item>
      <title>Comment un connecteur personnalisé Claude trouve-t-il le serveur OAuth d'osFoundry, et pourquoi l'accès est-il vérifié à chaque appel ?</title>
      <dc:creator>BBT</dc:creator>
      <pubDate>Fri, 06 Nov 2026 10:39:16 +0000</pubDate>
      <link>https://dev.to/bbt-fr/comment-un-connecteur-personnalise-claude-trouve-t-il-le-serveur-oauth-dosfoundry-et-pourquoi-4npf</link>
      <guid>https://dev.to/bbt-fr/comment-un-connecteur-personnalise-claude-trouve-t-il-le-serveur-oauth-dosfoundry-et-pourquoi-4npf</guid>
      <description>&lt;p&gt;Voici ce que reçoit un client MCP distant quand il appelle un outil d'&lt;a href="https://osfoundry.io/fr" rel="noopener noreferrer"&gt;osFoundry&lt;/a&gt; sans jeton. La requête a été envoyée avec curl depuis un Mac à Tokyo le 2026-11-06 :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ 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\", ..."]}}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cet unique en-tête suffit au connecteur personnalisé de Claude, ou à la connexion MCP du mode développeur de ChatGPT, pour trouver la page de connexion. L'utilisateur ne fait que coller l'URL. Tout ce qui suit vient de requêtes non authentifiées vers l'endpoint en production, plus une petite reconstruction en Python, exécutée en local, qui mesure ce qu'un curl ne peut pas montrer : ce qui arrive à l'accès après la connexion.&lt;/p&gt;

&lt;h2&gt;
  
  
  Que voit le client avant d'avoir un jeton ?
&lt;/h2&gt;

&lt;p&gt;Trois appels, trois réponses différentes :&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Requête&lt;/th&gt;
&lt;th&gt;Statut&lt;/th&gt;
&lt;th&gt;Ce qui revient&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;initialize&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;protocole &lt;code&gt;2025-06-18&lt;/code&gt;, serveur &lt;code&gt;osfoundry 0.1.0&lt;/code&gt;, plus des instructions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tools/list&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;59 outils, 45,368 octets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tools/call&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;401&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;WWW-Authenticate&lt;/code&gt; qui pointe vers les métadonnées de la ressource&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;GET&lt;/code&gt; sur l'endpoint&lt;/td&gt;
&lt;td&gt;405&lt;/td&gt;
&lt;td&gt;pas de flux SSE en GET&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Le handshake et le catalogue d'outils sont publics. Seul l'appel d'un outil exige un jeton. Le host peut donc montrer à l'utilisateur quels outils existent avant que quiconque se connecte. Rien dans le catalogue n'est une donnée utilisateur, et la chaîne &lt;code&gt;instructions&lt;/code&gt; de &lt;code&gt;initialize&lt;/code&gt; est générique :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Use list_granted_resources first. Every Note edit must use the exact tab ID.
Local-only data is unavailable to cloud clients.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Chaque outil de &lt;code&gt;tools/list&lt;/code&gt; déclare le scope OAuth dont il a besoin et s'il fait seulement de la lecture :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"edit_record_item"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"annotations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"readOnlyHint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"destructiveHint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"openWorldHint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"securitySchemes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"oauth2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"scopes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"osfoundry:write"&lt;/span&gt;&lt;span class="p"&gt;]}]}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sur les 59 outils : 30 demandent &lt;code&gt;osfoundry:read&lt;/code&gt; et sont marqués &lt;code&gt;readOnlyHint: true&lt;/code&gt;, 26 demandent &lt;code&gt;osfoundry:write&lt;/code&gt;, et 3 demandent &lt;code&gt;osfoundry:run&lt;/code&gt; (&lt;code&gt;request_session_run&lt;/code&gt;, &lt;code&gt;run_graph_session&lt;/code&gt;, &lt;code&gt;cancel_graph_run&lt;/code&gt;). Un host peut s'appuyer sur &lt;code&gt;readOnlyHint&lt;/code&gt; pour décider quels appels nécessitent une demande de confirmation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Où le 401 l'envoie-t-il ?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;resource_metadata&lt;/code&gt; suit la RFC 9728 (OAuth Protected Resource Metadata). Le suffixe well-known se place juste après l'hôte, suivi du chemin de la ressource. Il ne s'ajoute pas à la fin de l'URL de la ressource :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.osfoundry.io/api/v1/external-mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"authorization_servers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"https://api.osfoundry.io"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"scopes_supported"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"osfoundry:read"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"osfoundry:write"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"osfoundry:run"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"osfoundry:models"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Si vous écrivez un client à la main, ce 404 sera votre première erreur. Lisez l'URL dans l'en-tête au lieu de la construire vous-même.&lt;/p&gt;

&lt;h2&gt;
  
  
  Que dit le serveur d'autorisation de lui-même ?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;/.well-known/oauth-authorization-server&lt;/code&gt; (RFC 8414) :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"issuer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.osfoundry.io"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"authorization_endpoint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.osfoundry.io/api/v1/external-oauth/authorize"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"token_endpoint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.osfoundry.io/api/v1/external-oauth/token"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"registration_endpoint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.osfoundry.io/api/v1/external-oauth/register"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"grant_types_supported"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"authorization_code"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"refresh_token"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"token_endpoint_auth_methods_supported"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"none"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"code_challenge_methods_supported"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"S256"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"authorization_response_iss_parameter_supported"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lisez-le comme une chaîne de décisions. Il y a un &lt;code&gt;registration_endpoint&lt;/code&gt;, donc le host peut s'enregistrer tout seul (enregistrement dynamique de client) et l'utilisateur ne manipule jamais de client ID. L'authentification au token endpoint est &lt;code&gt;none&lt;/code&gt;, donc le host est un client public sans client secret, et c'est PKCE en S256 qui lie le code au client qui l'a demandé. &lt;code&gt;iss&lt;/code&gt; dans la réponse d'autorisation évite les confusions quand un host parle à plusieurs serveurs d'autorisation.&lt;/p&gt;

&lt;p&gt;L'étape authorize envoie le navigateur sur la page de consentement d'osFoundry, &lt;code&gt;https://osfoundry.io/external-assistant-authorize&lt;/code&gt;. L'utilisateur y choisit exactement un workspace et coche "I approve connecting this assistant to …" (j'autorise la connexion de cet assistant à …). L'approbation désigne un workspace et rien d'autre. Une nouvelle connexion n'a aucun projet. Le propriétaire les ajoute ensuite dans l'app (key icon → Connected → Add to project).&lt;/p&gt;

&lt;h2&gt;
  
  
  Pourquoi le scope ne suffit-il pas ?
&lt;/h2&gt;

&lt;p&gt;Un scope est figé au moment où le jeton est émis. &lt;code&gt;osfoundry:write&lt;/code&gt; dit que la connexion peut effectuer des actions d'écriture. Il ne dit pas quelle Note, et il ne peut pas savoir que la Note a été déplacée dans un autre projet il y a dix minutes. Les notes de conception d'osFoundry listent ce qui est relu à chaque appel : statut de la connexion, workspace, appartenance au projet, projet actuel et ACL de la ressource, appartenance au canal ou rôle dans la session, l'outil et le scope OAuth. La personne qui a approuvé la connexion doit aussi avoir encore les droits d'édition sur l'élément. Un retrait ou une réaffectation "prend effet immédiatement".&lt;/p&gt;

&lt;p&gt;L'alternative consiste à mettre la liste des projets dans le jeton, ou dans un cache indexé par le jeton, et à lui faire confiance jusqu'à expiration. C'est moins coûteux à écrire. La reconstruction ci-dessous implémente les deux pour pouvoir mesurer la différence de comportement.&lt;/p&gt;

&lt;h2&gt;
  
  
  La reconstruction
&lt;/h2&gt;

&lt;p&gt;Aucun binaire Claude n'intervient. &lt;code&gt;client.py&lt;/code&gt; joue le host et &lt;code&gt;server.py&lt;/code&gt; joue le serveur MCP. Les deux n'utilisent que la bibliothèque standard, environ 280 lignes à eux deux, et ont tourné sous Python 3.9.6 sur un Apple M1. Le serveur implémente &lt;code&gt;initialize&lt;/code&gt;, &lt;code&gt;tools/list&lt;/code&gt; et &lt;code&gt;tools/call&lt;/code&gt; en JSON-RPC sur HTTP simple, les deux documents well-known, &lt;code&gt;/authorize&lt;/code&gt; (la page de consentement est approuvée automatiquement pour un workspace) et &lt;code&gt;/token&lt;/code&gt; avec PKCE S256. Quelques routes &lt;code&gt;/admin/*&lt;/code&gt; remplacent les clics du propriétaire dans l'onglet Connected.&lt;/p&gt;

&lt;p&gt;La vérification exécutée avant chaque outil :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;access&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token_info&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;note_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;TOOL_SCOPE&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;token_info&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scopes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;insufficient_scope&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;CHECK&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token_only&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                     &lt;span class="c1"&gt;# design B: trust issue-time state
&lt;/span&gt;        &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;note_id&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;NOTES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;note_id&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;project&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;token_info&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;projects_at_issue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;not_in_project&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CONNECTIONS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token_info&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;connection&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;active&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;connection_revoked&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;note_id&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;note&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NOTES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;note_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;note&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;workspace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;workspace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;not_found&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="nf"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token_info&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;connection&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;project&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;PROJECT_MEMBERS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;not_in_project&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Le client démarre sans jeton. Il appelle un outil, reçoit le 401, extrait &lt;code&gt;resource_metadata&lt;/code&gt; de &lt;code&gt;WWW-Authenticate&lt;/code&gt;, récupère les deux documents de métadonnées, enchaîne authorize et token avec un nouveau verifier PKCE, puis déroule le scénario :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ CHECK=per_call python3 server.py &amp;amp;  python3 client.py 8767
discovery: first call -&amp;gt; 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
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Le même client face à &lt;code&gt;CHECK=token_only&lt;/code&gt;. Ici, le jeton porte la liste des projets de son dernier refresh :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Les étapes 1–7 sont identiques. L'étape 7 reste refusée dans la conception « jeton seul », parce que le projet de la note est consulté en direct. La conception « jeton seul » ne devient périmée que sur les faits stockés dans le jeton. Les étapes 8 et 9 sont celles qui comptent pour le propriétaire : il a retiré le projet, puis révoqué la connexion, et le chat a continué à lire. Dans la conception « jeton seul », cela dure jusqu'à l'expiration du jeton, soit &lt;code&gt;expires_in: 3600&lt;/code&gt; dans cette reconstruction.&lt;/p&gt;

&lt;p&gt;Les refus se répartissent sur deux couches. Un scope manquant est un échec au niveau du protocole : 403 avec &lt;code&gt;error="insufficient_scope"&lt;/code&gt; et le scope requis dans &lt;code&gt;WWW-Authenticate&lt;/code&gt;, pour qu'un host puisse en demander davantage. Tout ce qui dépend de l'élément concerné est un résultat d'outil normal avec &lt;code&gt;isError: true&lt;/code&gt;. Une nouvelle autorisation n'y changerait rien, donc inutile de dire au host de réessayer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Combien coûte la vérification à chaque appel ?
&lt;/h2&gt;

&lt;p&gt;Mesuré avec &lt;code&gt;time.perf_counter_ns()&lt;/code&gt; autour de &lt;code&gt;access()&lt;/code&gt; dans le serveur, 1,000 appels par exécution (500 autorisés, 500 refusés), sur un Mac qui faisait aussi tourner d'autres tâches :&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;access() médiane&lt;/th&gt;
&lt;th&gt;access() p99&lt;/th&gt;
&lt;th&gt;appel d'outil en loopback, médiane&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;vérification à chaque appel, 6 exécutions&lt;/td&gt;
&lt;td&gt;1.3–1.6 µs&lt;/td&gt;
&lt;td&gt;6.1–7.0 µs&lt;/td&gt;
&lt;td&gt;0.43–0.78 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;jeton seul, 3 exécutions&lt;/td&gt;
&lt;td&gt;0.7–1.6 µs&lt;/td&gt;
&lt;td&gt;3.5–7.7 µs&lt;/td&gt;
&lt;td&gt;0.35–1.36 ms (2 exécutions)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;En mémoire, la revérification se noie dans le bruit d'un appel HTTP en loopback. Un vrai serveur fait des requêtes en base de données au lieu de tests d'appartenance à un ensemble, donc son coût est plus élevé. Cette reconstruction ne peut pas le mesurer, donc aucun chiffre n'est donné. Pour l'ordre de grandeur, le trajet réseau vers l'endpoint en production depuis Tokyo est d'environ 500 ms à 800 ms par requête avec urllib et une nouvelle connexion TLS à chaque fois (n=10 pour chacun : &lt;code&gt;initialize&lt;/code&gt; médiane 790 ms, &lt;code&gt;tools/list&lt;/code&gt; 816 ms, le &lt;code&gt;tools/call&lt;/code&gt; en 401 565 ms, chaque document de métadonnées environ 520–530 ms). Ce n'est pas dans la vérification d'accès qu'un connecteur passe son temps.&lt;/p&gt;

&lt;p&gt;La découverte elle-même représentait 5 allers-retours HTTP en local : 401, métadonnées de la ressource, métadonnées du serveur, authorize, token. Cela a pris 7.5–13.4 ms sur sept exécutions, et 34 ms une fois avec le Mac chargé. Face au serveur en production, les trois trajets GET/POST faisables sans identifiants totalisent environ 1.6 s à froid. L'étape authorize attend de toute façon un humain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pourquoi Claude peut-il appeler osFoundry, alors qu'osFoundry ne peut pas ouvrir vos chats Claude ?
&lt;/h2&gt;

&lt;p&gt;Dans ce montage, c'est le host qui appelle. Il ouvre la connexion, liste les outils et décide quand en appeler un. Le serveur répond. Rien dans ce montage ne donne au serveur une prise sur les conversations du host. Le plan d'osFoundry l'indique dans son tableau des capacités : ouvrir ou énumérer des chats personnels ChatGPT/Claude arbitraires, c'est "No", avec "No general host API is provided for this".&lt;/p&gt;

&lt;p&gt;Cela façonne les outils. Avec &lt;code&gt;link_current_chat&lt;/code&gt;, c'est le chat lui-même qui crée un lien depuis l'intérieur. Son libellé est déclaré par le chat, et osFoundry s'en sert pour le routage, pas comme identité. Le travail assigné à un chat Claude lié attend que l'utilisateur ouvre ce chat et lui demande de vérifier. ChatGPT dispose de MCP Events pour les chats abonnés, et le plan d'osFoundry traite cela comme propre à ce host.&lt;/p&gt;

&lt;p&gt;Le côté serveur détient toutes les décisions sur les données, parce que c'est le seul à connaître l'état actuel d'un projet. Le côté host détient toutes les décisions sur le moment d'agir et sur l'opportunité de demander à l'utilisateur. La vérification à chaque appel est ce qui permet au serveur de rester correct quand les deux côtés se désynchronisent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Si vous câblez cela vous-même
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Mettez &lt;code&gt;resource_metadata&lt;/code&gt; dans le &lt;code&gt;WWW-Authenticate&lt;/code&gt; du 401 et servez la RFC 9728 à &lt;code&gt;/.well-known/oauth-protected-resource/&amp;lt;resource path&amp;gt;&lt;/code&gt;. Un client qui construit l'URL à la main peut se tromper. L'en-tête évite ça à tous les clients.&lt;/li&gt;
&lt;li&gt;Décidez de ce qui est public avant la connexion. osFoundry expose &lt;code&gt;initialize&lt;/code&gt; et &lt;code&gt;tools/list&lt;/code&gt; et protège &lt;code&gt;tools/call&lt;/code&gt;. Si les descriptions de vos outils laissent fuiter quoi que ce soit sur les tenants, protégez aussi &lt;code&gt;tools/list&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Déclarez un scope et &lt;code&gt;readOnlyHint&lt;/code&gt; par outil. Les hosts peuvent en tirer leurs demandes de confirmation, et un 403 &lt;code&gt;insufficient_scope&lt;/code&gt; donne au host quelque chose de concret à redemander.&lt;/li&gt;
&lt;li&gt;Gardez le jeton petit : qui, quelle connexion, quels scopes. Relisez à chaque appel le workspace, l'appartenance au projet, le projet de la ressource et le rôle actuel de la personne qui a approuvé, pour que la révocation et « déplacer vers un autre projet » s'appliquent dès l'appel suivant.&lt;/li&gt;
&lt;li&gt;Renvoyez les refus liés aux ressources comme résultats d'outil, pas en 401/403. Un autre jeton ne réglera pas « cette Note est dans le projet B ».&lt;/li&gt;
&lt;li&gt;Faites porter aux écritures un &lt;code&gt;expected_version&lt;/code&gt; et une clé d'idempotence. Les hosts réessaient, et les modèles relancent des appels.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Le côté MCP de la spécification se trouve sur la &lt;a href="https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization" rel="noopener noreferrer"&gt;page d'autorisation MCP&lt;/a&gt;. La définition courte d'osFoundry se trouve dans son &lt;a href="https://osfoundry.io/fr/glossary/mcp" rel="noopener noreferrer"&gt;entrée de glossaire sur MCP&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>oauth</category>
      <category>claude</category>
      <category>securite</category>
    </item>
  </channel>
</rss>
