<?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 Deutsch</title>
    <description>The latest articles on DEV Community by BBT Deutsch (bbt-de).</description>
    <link>https://dev.to/bbt-de</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%2F15897%2Fb14e15e1-20d1-43ea-89c5-bb624cbaaf21.png</url>
      <title>DEV Community: BBT Deutsch</title>
      <link>https://dev.to/bbt-de</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bbt-de"/>
    <language>en</language>
    <item>
      <title>Wie findet ein Claude Custom Connector den OAuth-Server von osFoundry, und warum wird der Zugriff bei jedem Aufruf geprüft?</title>
      <dc:creator>BBT</dc:creator>
      <pubDate>Fri, 06 Nov 2026 10:38:30 +0000</pubDate>
      <link>https://dev.to/bbt-de/wie-findet-ein-claude-custom-connector-den-oauth-server-von-osfoundry-und-warum-wird-der-zugriff-3904</link>
      <guid>https://dev.to/bbt-de/wie-findet-ein-claude-custom-connector-den-oauth-server-von-osfoundry-und-warum-wird-der-zugriff-3904</guid>
      <description>&lt;p&gt;Das bekommt ein Remote-MCP-Client zurück, wenn er ein Tool von &lt;a href="https://osfoundry.io/de" rel="noopener noreferrer"&gt;osFoundry&lt;/a&gt; ohne Token aufruft. Die Anfrage wurde am 2026-11-06 mit curl von einem Mac in Tokio geschickt:&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;Dieser eine Header reicht dem Custom Connector von Claude, oder der MCP-Verbindung im Developer Mode von ChatGPT, um die Anmeldeseite zu finden. Der Nutzer fügt nur die URL ein. Alles Folgende stammt aus nicht authentifizierten Anfragen an den Live-Endpoint, plus einem kleinen Nachbau in Python, lokal ausgeführt, der misst, was ein curl nicht zeigen kann: was nach der Anmeldung mit dem Zugriff passiert.&lt;/p&gt;

&lt;h2&gt;
  
  
  Was sieht der Client, bevor er ein Token hat?
&lt;/h2&gt;

&lt;p&gt;Drei Aufrufe, drei verschiedene Antworten:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Anfrage&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;Was zurückkommt&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;Protokoll &lt;code&gt;2025-06-18&lt;/code&gt;, Server &lt;code&gt;osfoundry 0.1.0&lt;/code&gt;, dazu 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 Tools, 45,368 Bytes&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; mit Verweis auf die Resource Metadata&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;GET&lt;/code&gt; auf den Endpoint&lt;/td&gt;
&lt;td&gt;405&lt;/td&gt;
&lt;td&gt;kein SSE-Stream per GET&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Handshake und Tool-Katalog sind öffentlich. Nur der Tool-Aufruf braucht ein Token. Dadurch kann der Host dem Nutzer zeigen, welche Tools es gibt, bevor sich überhaupt jemand anmeldet. Im Katalog stehen keine Nutzerdaten, und der &lt;code&gt;instructions&lt;/code&gt;-String in &lt;code&gt;initialize&lt;/code&gt; ist generisch:&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;Jedes Tool in &lt;code&gt;tools/list&lt;/code&gt; gibt an, welchen OAuth-Scope es braucht und ob es nur liest:&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;Über alle 59 Tools: 30 brauchen &lt;code&gt;osfoundry:read&lt;/code&gt; und sind mit &lt;code&gt;readOnlyHint: true&lt;/code&gt; markiert, 26 brauchen &lt;code&gt;osfoundry:write&lt;/code&gt;, und 3 brauchen &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;). Ein Host kann anhand von &lt;code&gt;readOnlyHint&lt;/code&gt; entscheiden, welche Aufrufe eine Bestätigung brauchen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wohin schickt ihn der 401?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;resource_metadata&lt;/code&gt; folgt RFC 9728 (OAuth Protected Resource Metadata). Das Well-known-Suffix kommt direkt hinter den Host, der Ressourcenpfad wird hinten angehängt. Es wird nicht an die Ressourcen-URL angehängt:&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;Wenn du einen Client von Hand baust, ist dieser 404 der erste Fehler, den du machst. Lies die URL aus dem Header, statt sie selbst zusammenzubauen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Was sagt der Authorization Server über sich selbst?
&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;Lies das als Kette von Entscheidungen. Es gibt einen &lt;code&gt;registration_endpoint&lt;/code&gt;, also kann sich der Host selbst registrieren (Dynamic Client Registration), und der Nutzer hat nie mit einer Client-ID zu tun. Die Token-Authentifizierung ist &lt;code&gt;none&lt;/code&gt;, also ist der Host ein Public Client ohne Client Secret, und PKCE mit S256 bindet den Code an den Client, der ihn angefordert hat. &lt;code&gt;iss&lt;/code&gt; in der Authorization Response schützt vor Verwechslungen, wenn ein Host mit mehreren Authorization Servern spricht.&lt;/p&gt;

&lt;p&gt;Der Authorize-Schritt schickt den Browser auf die Consent-Seite von osFoundry, &lt;code&gt;https://osfoundry.io/external-assistant-authorize&lt;/code&gt;. Dort wählt der Nutzer genau einen Workspace und hakt "I approve connecting this assistant to …" an (ich erlaube, diesen Assistenten zu verbinden mit …). Die Freigabe nennt einen Workspace und sonst nichts. Eine neue Verbindung hat keine Projekte. Die fügt der Owner danach in der App hinzu (key icon → Connected → Add to project).&lt;/p&gt;

&lt;h2&gt;
  
  
  Warum reicht der Scope nicht?
&lt;/h2&gt;

&lt;p&gt;Ein Scope steht fest, sobald das Token ausgestellt wird. &lt;code&gt;osfoundry:write&lt;/code&gt; sagt, dass die Verbindung schreibende Aktionen ausführen darf. Es sagt nicht, welche Note, und es kann nicht wissen, dass die Note vor zehn Minuten in ein anderes Projekt verschoben wurde. Die Design-Notizen von osFoundry listen auf, was bei jedem Aufruf neu gelesen wird: Verbindungsstatus, Workspace, Projektmitgliedschaft, das aktuelle Projekt und die ACL der Ressource, Channel-Mitgliedschaft oder Session-Rolle, das Tool und der OAuth-Scope. Außerdem muss die Person, die die Verbindung freigegeben hat, noch Bearbeitungsrechte auf das Element haben. Entfernen oder Neuzuweisen "wirkt sofort".&lt;/p&gt;

&lt;p&gt;Die Alternative: die Projektliste ins Token packen, oder in einen Cache mit dem Token als Schlüssel, und ihr bis zum Ablauf vertrauen. Das ist billiger zu schreiben. Der Nachbau unten implementiert beides, damit sich der Unterschied im Verhalten messen lässt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Der Nachbau
&lt;/h2&gt;

&lt;p&gt;Es ist kein Claude-Binary beteiligt. &lt;code&gt;client.py&lt;/code&gt; spielt den Host und &lt;code&gt;server.py&lt;/code&gt; den MCP-Server. Beide nutzen nur die Standardbibliothek, zusammen etwa 280 Zeilen, und liefen mit Python 3.9.6 auf einem Apple M1. Der Server implementiert &lt;code&gt;initialize&lt;/code&gt;, &lt;code&gt;tools/list&lt;/code&gt; und &lt;code&gt;tools/call&lt;/code&gt; über einfaches HTTP-JSON-RPC, die beiden Well-known-Dokumente, &lt;code&gt;/authorize&lt;/code&gt; (die Consent-Seite wird für einen Workspace automatisch bestätigt) und &lt;code&gt;/token&lt;/code&gt; mit S256-PKCE. Ein paar &lt;code&gt;/admin/*&lt;/code&gt;-Routen ersetzen die Klicks des Owners im Tab Connected.&lt;/p&gt;

&lt;p&gt;Die Prüfung, die vor jedem Tool läuft:&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;Der Client startet ohne Token. Er ruft ein Tool auf, bekommt den 401, parst &lt;code&gt;resource_metadata&lt;/code&gt; aus &lt;code&gt;WWW-Authenticate&lt;/code&gt;, holt beide Metadaten-Dokumente, macht Authorize und Token mit einem frischen PKCE-Verifier und arbeitet dann das Szenario ab:&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;Derselbe Client gegen &lt;code&gt;CHECK=token_only&lt;/code&gt;. Hier trägt das Token die Projektliste vom letzten 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;Die Schritte 1–7 sind identisch. Schritt 7 wird auch im Token-only-Design abgelehnt, weil das Projekt der Notiz live nachgeschlagen wird. Das Token-only-Design veraltet nur bei Fakten, die im Token stecken. Die Schritte 8 und 9 sind die, auf die es dem Owner ankommt: Er hat das Projekt entfernt, dann die Verbindung widerrufen, und der Chat hat weitergelesen. Im Token-only-Design hält das an, bis das Token abläuft, in diesem Nachbau &lt;code&gt;expires_in: 3600&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Ablehnungen verteilen sich auf zwei Ebenen. Ein fehlender Scope ist ein Fehler auf Protokollebene: 403 mit &lt;code&gt;error="insufficient_scope"&lt;/code&gt; und dem benötigten Scope in &lt;code&gt;WWW-Authenticate&lt;/code&gt;, damit ein Host mehr anfordern kann. Alles, was davon abhängt, um welches Element es geht, ist ein normales Tool-Ergebnis mit &lt;code&gt;isError: true&lt;/code&gt;. Eine erneute Autorisierung würde das nicht beheben, also hat es keinen Sinn, den Host dazu aufzufordern.&lt;/p&gt;

&lt;h2&gt;
  
  
  Was kostet die Prüfung pro Aufruf?
&lt;/h2&gt;

&lt;p&gt;Gemessen mit &lt;code&gt;time.perf_counter_ns()&lt;/code&gt; um &lt;code&gt;access()&lt;/code&gt; im Server, 1,000 Aufrufe pro Lauf (500 erlaubt, 500 abgelehnt), auf einem Mac, der nebenbei andere Jobs laufen hatte:&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() Median&lt;/th&gt;
&lt;th&gt;access() p99&lt;/th&gt;
&lt;th&gt;Loopback-Tool-Aufruf, Median&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Prüfung pro Aufruf, 6 Läufe&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;nur Token, 3 Läufe&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 Läufe)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Im Speicher geht die erneute Prüfung im Rauschen eines Loopback-HTTP-Aufrufs unter. Ein echter Server macht Datenbankabfragen statt Set-Lookups, die Kosten sind also höher. Das kann dieser Nachbau nicht messen, deshalb gibt es dafür keine Zahl. Zur Einordnung: Die Netzwerkstrecke zum Live-Endpoint aus Tokio liegt bei etwa 500 ms bis 800 ms pro Anfrage mit urllib und jedes Mal neuer TLS-Verbindung (je n=10: &lt;code&gt;initialize&lt;/code&gt; Median 790 ms, &lt;code&gt;tools/list&lt;/code&gt; 816 ms, der 401-&lt;code&gt;tools/call&lt;/code&gt; 565 ms, jedes Metadaten-Dokument etwa 520–530 ms). Die Zugriffsprüfung ist nicht der Punkt, an dem ein Connector seine Zeit verbringt.&lt;/p&gt;

&lt;p&gt;Die Discovery selbst waren lokal 5 HTTP-Roundtrips: 401, Resource Metadata, Server Metadata, Authorize, Token. Das dauerte 7.5–13.4 ms in sieben Läufen und einmal 34 ms, als der Mac ausgelastet war. Gegen den Live-Server summieren sich die drei GET/POST-Strecken, die ohne Credentials möglich sind, kalt auf ungefähr 1.6 s. Die Authorize-Strecke wartet sowieso auf einen Menschen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Warum kann Claude osFoundry aufrufen, aber osFoundry deine Claude-Chats nicht öffnen?
&lt;/h2&gt;

&lt;p&gt;In diesem Aufbau ist der Host der Aufrufer. Er öffnet die Verbindung, listet die Tools und entscheidet, wann er eines aufruft. Der Server antwortet. Nichts in diesem Aufbau gibt dem Server einen Zugriff auf die Unterhaltungen des Hosts. Der Plan von osFoundry sagt das in seiner Capability-Tabelle: Beliebige persönliche ChatGPT/Claude-Chats öffnen oder auflisten ist "No", mit "No general host API is provided for this".&lt;/p&gt;

&lt;p&gt;Das prägt die Tools. Bei &lt;code&gt;link_current_chat&lt;/code&gt; legt der Chat den Link von innen selbst an. Sein Label gibt der Chat selbst an, und osFoundry nutzt es zum Routing, nicht als Identität. Arbeit, die einem verknüpften Claude-Chat zugewiesen ist, wartet, bis der Nutzer diesen Chat öffnet und ihn bittet nachzusehen. ChatGPT hat MCP Events für abonnierte Chats, und der Plan von osFoundry behandelt das als hostspezifisch.&lt;/p&gt;

&lt;p&gt;Die Serverseite trifft alle Entscheidungen über Daten, weil nur sie den aktuellen Stand eines Projekts kennt. Die Hostseite trifft alle Entscheidungen darüber, wann gehandelt wird und ob der Nutzer gefragt wird. Die Prüfung pro Aufruf sorgt dafür, dass der Server korrekt bleibt, wenn die beiden Seiten auseinanderlaufen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wenn du das selbst verdrahtest
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Pack &lt;code&gt;resource_metadata&lt;/code&gt; in den &lt;code&gt;WWW-Authenticate&lt;/code&gt; des 401 und liefere RFC 9728 unter &lt;code&gt;/.well-known/oauth-protected-resource/&amp;lt;resource path&amp;gt;&lt;/code&gt; aus. Ein Client, der die URL selbst baut, kann sie falsch bauen. Der Header erspart das jedem Client.&lt;/li&gt;
&lt;li&gt;Leg fest, was vor der Anmeldung öffentlich ist. osFoundry gibt &lt;code&gt;initialize&lt;/code&gt; und &lt;code&gt;tools/list&lt;/code&gt; frei und schützt &lt;code&gt;tools/call&lt;/code&gt;. Wenn deine Tool-Beschreibungen etwas über Tenants verraten, schütz auch &lt;code&gt;tools/list&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Deklarier pro Tool einen Scope und &lt;code&gt;readOnlyHint&lt;/code&gt;. Hosts können daraus Bestätigungsdialoge bauen, und ein 403 &lt;code&gt;insufficient_scope&lt;/code&gt; gibt dem Host etwas Konkretes, das er neu anfordern kann.&lt;/li&gt;
&lt;li&gt;Halt das Token klein: wer, welche Verbindung, welche Scopes. Lies Workspace, Projektmitgliedschaft, das Projekt der Ressource und die aktuelle Rolle der freigebenden Person bei jedem Aufruf neu, damit Widerruf und "in ein anderes Projekt verschieben" beim nächsten Aufruf greifen.&lt;/li&gt;
&lt;li&gt;Gib Ablehnungen auf Ressourcenebene als Tool-Ergebnisse zurück, nicht als 401/403. Ein anderes Token behebt "diese Note liegt in Projekt B" nicht.&lt;/li&gt;
&lt;li&gt;Lass Schreibvorgänge &lt;code&gt;expected_version&lt;/code&gt; und einen Idempotency Key mitschicken. Hosts wiederholen Anfragen, und Modelle setzen Aufrufe erneut ab.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Die MCP-Seite der Spezifikation steht auf der &lt;a href="https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization" rel="noopener noreferrer"&gt;Autorisierungsseite von MCP&lt;/a&gt;. Die kurze Definition von osFoundry steht im &lt;a href="https://osfoundry.io/de/glossary/mcp" rel="noopener noreferrer"&gt;Glossareintrag zu MCP&lt;/a&gt;.&lt;/p&gt;

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