Skip to main content
Glama

onenote-mcp

Ein MCP-Server, der Microsoft OneNote über Microsoft Graph verfügbar macht — Notizbuch- und Abschnittsstruktur, Seiteninhalte und Handschrift, die als Bild gerendert wird, das das aufrufende Modell lesen kann.

project-spec.md ist das maßgebliche Designdokument. Es behandelt die Tinten-Rekonstruktions-Pipeline, die zwei unabhängigen OAuth-Ebenen, das Cloud-Run- Bereitstellungsmodell und den Firestore-gestützten Token-Cache. Lesen Sie es, bevor Sie hier etwas ändern.

Voraussetzungen

Node >= 24. @google-cloud/firestore erfordert Node >= 22, und Node 24 ist das aktuelle Active LTS.

Related MCP server: OneNoteMCP

Schnellstart

npm ci
npm run build
npm test

Skripte

Skript

Was es tut

npm run build

Kompiliert src/ nach dist/

npm run typecheck

Typprüfung ohne Ausgabe

npm run dev

Führt den Server aus dem Quellcode mit --watch aus

npm start

Führt den kompilierten Server aus dist/ aus (build zuerst ausführen)

npm run bootstrap

Lokale Gerätecode-Anmeldung, die den Firestore-Token-Cache befüllt

npm test

node --test über test/**/*.test.ts

Die Tests liegen in test/ und spiegeln src/ wider. Sie laufen direkt gegen den TypeScript-Quellcode mit nativem Type-Stripping von Node, daher benötigt npm test keinen Build. Das schränkt den Quellcode ein: kein enum, kein namespace, keine Konstruktor-Parameter-Eigenschaften, und reine Typ-Importe müssen als import type geschrieben werden. Die Compiler-Optionen erasableSyntaxOnly und verbatimModuleSyntax erzwingen dies.

Siehe CLAUDE.md für das Verzeichnislayout und die damit verbundenen Konventionen.

Token-Cache

src/token-cache.ts implementiert MSALs ICachePlugin gegen ein einzelnes Firestore-Dokument, dessen Pfad aus FIRESTORE_CACHE_DOC stammt. beforeCacheAccess liest das cache-Feld des Dokuments und übergibt die Zeichenkette an MSAL. afterCacheAccess schreibt den serialisierten Cache innerhalb einer Firestore-Transaktion zurück, und nur wenn MSAL meldet, dass sich der Cache geändert hat. Ein nicht vorhandenes Dokument wird als leerer Cache gelesen, was dem Zustand vor dem Ausführen von npm run bootstrap entspricht. Beide Einstiegspunkte verwenden dasselbe Plugin: Die Bootstrap-CLI schreibt den Cache darüber und der Server liest darüber, sodass es einen Serialisierer und kein zweites Format gibt, das synchron gehalten werden müsste.

Dieser Blob ist die einzige Kopie des Refresh-Tokens, daher schützen ihn zwei Dinge.

Ein Schreibvorgang, der das Dokument leeren würde, wird verweigert. MSAL entfernt Anmeldeinformationen bei einigen Fehlern aus seinem In-Memory-Cache, und afterCacheAccess läuft innerhalb des finally-Blocks von MSAL, sodass eine Serialisierung, die das Konto verloren hat, diesen Code erreichen kann, während die gespeicherte noch gültig ist. overwriteWouldEmptyCache stoppt es und protokolliert {"event":"token-cache-write-refused"}. Die Leerheitsprüfung liest keinen MSAL-Schlüsselnamen — ein Cache ist leer, wenn er zu einem Objekt geparst wird, dessen jeder Wert ein leerer Container ist —, sodass sie sich nicht umkehren kann, wenn MSAL sein Format ändert, und alles, was sie nicht erkennt, wird durchgelassen statt blockiert.

Der Blob, den jeder Schreibvorgang ersetzt, wird in einem previousCache-Feld aufbewahrt. Eine Generation, keine Historie: Der Cache wird bei jeder Aktualisierung neu geschrieben, und die nützliche Kopie ist immer die jüngste gute. Die Wiederherstellung nach einem fehlerhaften Schreibvorgang besteht darin, dieses Feld in der Firestore-Konsole über cache zu kopieren, was sich lohnt, weil die Alternative eine Gerätecode-Anmeldung ist. Aktivieren Sie die Point-in-Time-Wiederherstellung für eine zweite Ebene:

gcloud firestore databases update --enable-pitr

Ein Backend-Fehler ist kein Anmeldefehler. Wenn Firestore nicht erreichbar ist oder eine widerrufene roles/datastore.user-Bindung vorliegt, wird TokenCacheUnavailableError ausgelöst, statt als der Fehler zu erscheinen, den ein toter Refresh-Token erzeugt. Schreibvorgänge werden dreimal wiederholt, bevor das passiert. Siehe die Zeile cache-unavailable in der Tabelle unten, warum die Unterscheidung den Code wert ist.

npm test deckt nur readCache ab, die Funktion, die eine Dokument-Snapshot dekodiert. Die beiden Callbacks, die Transaktion und createFirestoreTokenCachePlugin haben keinen automatisierten Test — sie benötigen ein Firestore-Backend. Sie zu testen bedeutet den Emulator, der java auf PATH und eine eigene Installation benötigt:

sudo apt-get install google-cloud-cli-firestore-emulator

gcloud components install cloud-firestore-emulator installiert es nicht auf einer Debian-paketierten Google Cloud CLI. Der Komponentenmanager ist in diesem Build deaktiviert, und gcloud gibt stattdessen den apt-get-Befehl oben aus.

Graph-Authentifizierung

src/graph-auth.ts wandelt den befüllten Token-Cache in ein Microsoft-Graph-Zugriffstoken um. createGraphAuth erstellt eine PublicClientApplication aus ONENOTE_CLIENT_ID, ONENOTE_AUTHORITY und dem Firestore-Cache-Plugin und hält sie für die Lebensdauer des Prozesses. getAccessToken() liest das zwischengespeicherte Konto, ruft acquireTokenSilent auf und gibt das Token zurück. Angeforderte Bereiche sind Notes.Read und Notes.ReadWrite, vollständig qualifiziert.

Der bereitgestellte Server meldet sich nie interaktiv an. Er hat keine Möglichkeit, jemanden aufzufordern, und die OneNote-Endpunkte von Graph unterstützen keine App-only-Authentifizierung, daher gibt es keinen Fallback, wenn das gespeicherte Refresh-Token stirbt — ein Mensch führt npm run bootstrap erneut aus. Jeder Fehler ist daher ein GraphAuthError, der dies sagt, statt eines rohen MSAL-Fehlers, der den Aufrufer als nackten 401 von Graph erreichen würde:

reason

Was passiert ist

Was zu tun ist

cache-unreadable

Das Firestore-Dokument fehlt, oder sein cache-Feld ist nichts, das MSAL deserialisieren kann

npm run bootstrap

cache-unavailable

Firestore hat nicht geantwortet, oder das Laufzeit-Dienstkonto hat roles/datastore.user verloren

Wiederholen. Keine Anmeldung.

no-account

Der Cache wurde gelesen, enthält aber kein angemeldetes Konto

npm run bootstrap

silent-failed

Das gespeicherte Refresh-Token ist abgelaufen oder widerrufen, oder der Token-Endpunkt hat nichts Brauchbares zurückgegeben

npm run bootstrap

cache-unavailable ist die Zeile, die sich lohnt. Firestore wird innerhalb von acquireTokenSilent über das Cache-Plugin gelesen und geschrieben, daher kam ein Backend-Ausfall früher als dieselbe Ablehnung an, die ein toter Refresh-Token erzeugt — und diese Meldung sagt dem Betreiber, er solle zu einem Browser gehen und eine Anmeldeinformation ersetzen, die funktioniert. GraphAuthError.retryable trägt die Unterscheidung, und nur dieser Grund setzt sie.

Jeder dieser Fälle schreibt außerdem eine Zeile nach stderr:

{"event":"graph-auth-failure","reason":"silent-failed","documentPath":"tokencache/msal","retryable":"false"}

Diese Zeile ist der Punkt. Ein Tool-Fehler erscheint sonst nur innerhalb einer Claude-Konversation, sodass ohne sie nichts dem Betreiber mitteilt, dass der Connector aufgehört hat zu funktionieren. Siehe Alerting unten.

Die Meldungen nennen den Dokumentpfad und den zugrunde liegenden MSAL-Fehler und tragen bewusst keine Konto-Kennung: username ist der UPN des Benutzers und homeAccountId enthält die Mandanten-ID, von denen keines in ein Protokoll gehört.

npm test deckt die Erfassungslogik über einen Fake-Client ab. createGraphAuth selbst hat keinen automatisierten Test: Es benötigt einen Cache, der durch eine echte Gerätecode-Anmeldung befüllt wurde, und keine Anmeldeinformation, die einen solchen befüllen könnte, darf committet werden. Führen Sie npm run bootstrap und dann den Server gegen dasselbe Dokument aus, um es zu testen. Sein Konsument ist der Graph-Struktur-Client unten; nichts verdrahtet eines von beiden bisher in createApp.

Graph-Struktur

src/graph-structure.ts liest den OneNote-Baum: Notizbücher, Abschnittsgruppen, Abschnitte und die Seitenliste innerhalb eines Abschnitts. new GraphStructure(auth) nimmt alles mit einer getAccessToken()-Methode, sodass der Server ihm die GraphAuth oben übergibt.

Methode

Rückgabe

listNotebooks()

Jedes Notizbuch, nach Anzeigename

listSections(containerKind, containerId)

Abschnitte direkt unter einem Notizbuch oder einer Abschnittsgruppe

listSectionGroups(containerKind, containerId)

Abschnittsgruppen direkt unter einem Notizbuch oder einer Abschnittsgruppe

listContainerChildren(containerKind, containerId)

Beide oben genannten, zusammen abgerufen

listPagesInSection(sectionId, top?)

Seiten in einem Abschnitt, zuletzt geändert zuerst, höchstens top (Standard 50)

getNotebookTree(notebook)

Ein Notizbuch mit jeder verschachtelten Abschnittsgruppe aufgelöst

getFullTree()

Jedes Notizbuch, jeweils mit aufgelöstem Baum

getExpandedTree()

Jedes Notizbuch mit seinen Abschnitten und einer Ebene Abschnittsgruppe, in einer einzigen Anfrage

findSectionsByName(displayName)

Abschnitte überall im Konto, deren Name diesen Text enthält, jeweils mit übergeordnetem Notizbuch und Abschnittsgruppe

findPagesByTitle(sectionId, title)

Seiten in einem Abschnitt, deren Titel übereinstimmt, von Graph case-insensitiv verglichen

containerKind ist notebooks oder sectionGroups — die beiden Graph-Beziehungsnamen. Beide Container-Arten legen dieselben untergeordneten Beziehungen offen, weshalb die Listenmethoden die Art annehmen, statt zweimal zu existieren.

getExpandedTree() ist die günstige Variante. Sie bittet Graph, die Beziehungen zu erweitern, statt sie zu durchlaufen:

GET /me/onenote/notebooks?$select=id,displayName
    &$expand=sections($select=id,displayName),
             sectionGroups($select=id,displayName;$expand=sections($select=id,displayName))

Gemessen an einem Konto mit 54 Notizbüchern: eine Anfrage und 78 KB, gegenüber 195 Anfragen für getFullTree(), was wichtig ist, weil OneNote 400 Anfragen pro Stunde und 5 gleichzeitig erlaubt. Das $select innerhalb jeder Expand-Klausel reduziert die Antwort von 441 KB auf 78 KB, und das Trennzeichen innerhalb einer Klausel, die sowohl $select als auch $expand trägt, ist ein Semikolon. Was es nicht erreicht, ist eine Abschnittsgruppe, die in einer Abschnittsgruppe verschachtelt ist — Graph begrenzt die $expand-Verschachtelung auf zwei Ebenen —, daher deckt findSectionsByName diesen Fall stattdessen in einer Anfrage ab, indem es die kontoweite Abschnittsliste filtert und die übergeordneten Elemente jedes Abschnitts erweitert.

api-overview.md dokumentiert, was diese Endpunkte akzeptieren, einschließlich der Stellen, an denen der Dienst seiner eigenen Dokumentation widerspricht.

Drei Dinge, die die Traversierung behandelt, die ein einzelner Graph-Aufruf nicht tut:

  • Verschachtelung. Abschnittsgruppen sind die "Registerkartengruppen" der Benutzeroberfläche, und sie enthalten weitere Abschnittsgruppen. getNotebookTree rekursiert.

  • Paginierung. Jeder Listenaufruf folgt @odata.nextLink, bis er nicht mehr erscheint. Graph wählt seine eigene Seitengröße und ignoriert ein größeres $top, daher ist eine Antwort nie ein Beweis dafür, dass eine Sammlung vollständig ist. listPagesInSection stoppt, sobald top Elemente vorliegen, daher ist top eine Ergebnisanzahl und keine Seitengröße.

  • Die kontoweite Seitenliste wird nie aufgerufen. GET /me/onenote/pages schlägt mit Fehler 20266, "maximum sections exceeded", bei einer Notizbuch-pro-Jahr-Struktur fehl. Die Seitenauflistung ist immer auf /me/onenote/sections/{id}/pages beschränkt, und ein Test durchsucht src/ nach dem kontoweiten Pfad.

Fehler sind GraphRequestError bei einer Nicht-2xx-Antwort – er trägt status, statusText und den Antwort-body, denn Fehler 20266 ist nur durch diesen Text von jedem anderen 400 unterscheidbar – und GraphResponseError bei einer 2xx-Antwort, deren Body nicht die erwartete Form hat, bei einer Auflistung, die nicht endet, oder bei Abschnittsgruppen, die tiefer als 20 Ebenen verschachtelt sind. Keine Nachricht enthält einen Notizbuch-, Abschnitts- oder Seitennamen.

npm test treibt das Ganze durch ein gefälschtes fetch, das nach exakter URL schlüsselt. Was das nicht prüfen kann, ist, ob Graph diese URLs akzeptiert; die Query-Strings stammen aus dem validierten Recon-Skript in Anhang A von project-spec.md und sind nur durch einen Lauf gegen den echten Mandanten bestätigt.

Ink

Graphs normaler Seiteninhalts-Endpunkt verwirft Handschrift und hinterlässt <!-- InkNode is not supported -->, und Graph kann eine Seite weder als Bild noch als PDF exportieren. Handschrift wird daher aus den rohen Strichdaten rekonstruiert: GET /me/onenote/pages/{id}/content?includeInkML=true antwortet mit multipart/mixed, ein Teil ist dasselbe HTML, ein anderer das InkML. Die Striche werden zu einem SVG und dann zu einem PNG, das dem aufrufenden Modell als Bild zur eigenen visuellen Erfassung zugeht. Kein OCR-Dienst ist beteiligt.

Modul

Was es tut

src/multipart.ts

splitMultipart(body, contentType) → die Teile oder null, wenn die Antwort nicht multipart ist

src/ink.ts

parseInkStrokes(text) → Striche; strokesToSvg; rasterizeSvg; renderInk(text, width?) → ein PNG oder null

src/page-content.ts

GraphPageContent.fetchRaw(pageId) → die aufgeteilte Antwort; .fetchInk(pageId) → das PNG oder null

Vier Details entscheiden, ob das überhaupt funktioniert, und alle vier stammen aus dem validierten Recon-Skript in Anhang A von project-spec.md:

  • Namensräume werden entfernt. Graph emittiert inkml:ink, inkml:trace, inkml:traceFormat. fast-xml-parser ist mit removeNSPrefix: true konfiguriert, und jede Suche verwendet den nackten Namen.

  • Die Kanalreihenfolge kommt aus <traceFormat>. Die Punkte dieses Kontos sind X, Y, F, wobei F der Stiftdruck ist. Wenn man die ersten beiden Zahlen jedes Punkts liest, wird der Druck als Koordinate gezeichnet.

  • Koordinaten sind himetrisch. px = himetric * 96 / 2540. Das ist derselbe Koordinatenraum, in dem das Seiten-HTML getippten Inhalt positioniert, sodass Ink und getippter Inhalt später rechnerisch gegeneinander registriert werden könnten.

  • Spuren können überall im Baum stehen. Eine Seite kann mehr als eine <ink>-Wurzel tragen, und <traceGroup>-Elemente verschachteln sich. Alle werden gesammelt.

Eine Seite ohne Ink ergibt null. Das ist die normale Antwort für eine getippte Seite, kein Fehler. Die Fehler, die tatsächlich ausgelöst werden, sind InkParseError für Spurgruppen, die tiefer als 50 Ebenen verschachtelt sind, und InkRenderError für ein Dokument, das resvg ablehnt; keine der Nachrichten reproduziert das Dokument, denn Strichkoordinaten sind die Handschrift des Benutzers.

test/fixtures/*.inkml sind handverfasst – ein paar Striche, X/Y/F-Kanalreihenfolge, himetrische Einheiten, eine Datei mit zwei <ink>-Wurzeln und verschachtelten <traceGroup>-Elementen. Kein erfasster Seiten-Dump darf eingecheckt werden: gerendertes Ink ist vollständig lesbare persönliche Notizen.

MCP-Endpunkt

Der Server spricht MCP über zustandsloses Streamable HTTP unter POST /mcp. Jede Anfrage baut ihren eigenen MCP-Server auf, beantwortet sie und baut ihn wieder ab; nichts überlebt bis zur nächsten. Es gibt keine Session-ID und kein SSE – GET /mcp und DELETE /mcp werden mit 405 beantwortet, und ein POST antwortet mit einem JSON-Body, statt einen Stream zu öffnen. Ein offener Stream würde eine Cloud-Run-Instanz am Leben halten und Leerlaufzeit in Rechnung stellen.

curl -s -X POST localhost:8080/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
# {"result":{"tools":[]},"jsonrpc":"2.0","id":1}

Beide Accept-Typen sind laut Streamable-HTTP-Spezifikation erforderlich, obwohl dieser Server nie streamt. createTools in src/tools.ts ist das Register – sechs Browsing-Werkzeuge (list_notebooks, list_sections, list_pages, search_pages, find_page_by_name, list_pages_by_name), ein Lesewerkzeug (get_page_content) und drei Schreibwerkzeuge (append_to_page, create_page, update_page_title) – und src/mcp-server.ts ist die JSON-RPC-Oberfläche darum herum.

Ein Werkzeug, das eine Ausnahme wirft, kommt als Werkzeug-Ergebnis mit isError: true und einer lesbaren Nachricht zurück – ein abgelaufener Refresh-Token, eine Seite, die nicht mehr existiert, und ein Dokument, das resvg ablehnt, sind allesamt normale Ergebnisse, keine Protokollfehler. Nur ein Aufruf eines Werkzeugs, das nie registriert wurde, ist ein JSON-RPC-Fehler.

Jede Anfrage schreibt eine JSON-Logzeile: das HTTP-Verb, den Pfad, den Status, die Dauer, die JSON-RPC-Methode und bei einem tools/call den Werkzeugnamen. Niemals die Query-String, die Header, die Argumente oder das Ergebnis – siehe src/logging.ts.

/mcp ist hinter einem Bearer-Token geschlossen – siehe Bearer-Tokens auf dem MCP-Endpunkt. Der Health-Endpunkt bleibt offen.

OAuth-Discovery

Claude muss den Autorisierungsserver finden, bevor es einen Ablauf starten kann. src/oauth-router.ts mountet die mcpAuthRouter des SDKs an der Anwendungswurzel – sie baut ihre Pfade aus der Issuer-URL und nicht aus einem Mount-Punkt, kann also nicht hinter ein Präfix – und bedient fünf Routen, alle aus Notwendigkeit unauthentifiziert:

Pfad

Was es ist

GET /.well-known/oauth-authorization-server

RFC-8414-Autorisierungsserver-Metadaten

GET /.well-known/oauth-protected-resource/mcp

RFC-9728-Metadaten der geschützten Ressource

GET,POST /authorize

Autorisierungs-Endpunkt

POST /consent

Wohin das Einwilligungsformular postet; die /authorize-Router des SDKs besitzt keinen Resume-Pfad

POST /token

Token-Endpunkt

curl -s localhost:8080/.well-known/oauth-authorization-server
curl -s localhost:8080/.well-known/oauth-protected-resource/mcp

Alles in beiden Dokumenten leitet sich aus MCP_PUBLIC_URL ab: Es ist der Issuer, und die resource-Kennung ist es plus /mcp. MCP_PUBLIC_URL wird beim Start abgelehnt, wenn es einen abschließenden Schrägstrich trägt, damit jede durch Anhängen eines Pfads gebaute URL wohlgeformt ist; das issuer-Feld meldet dann die URL-normalisierte Form, die bei einem Nur-Origin-Wert dieselbe Zeichenkette mit wieder angehängtem Schrägstrich ist. Das Dokument der geschützten Ressource wird nur unter der pfadsuffigierten URL ausgeliefert – das nackte /.well-known/oauth-protected-resource ist eine 404, ebenso /.well-known/openid-configuration. Claude testet zuerst den suffigierten Pfad.

scopes_supported listet offline_access auf, was Claude dazu bringt, einen Refresh-Token zu verlangen, statt bei jedem Ablauf eines Zugriffstokens erneut einzuwilligen. Es gibt keinen registration_endpoint: Client-ID und -Geheimnis sind konfiguriert, Dynamic Client Registration hat also nichts zu tun. Ein Client ist registriert, mit drei Redirect-URIs – https://claude.ai/api/mcp/auth_callback für die gehosteten Claude-Oberflächen und http://localhost/callback plus http://127.0.0.1/callback für Claude Code, dessen Port gemäß RFC 8252 ignoriert wird.

GET /authorize rendert eine Einwilligungsseite, statt umzuleiten: eine Genehmigen-Schaltfläche, die benennt, was gewährt wird, und den Host, an den der Autorisierungscode gesendet wird. Das Genehmigen postet an POST /consent zurück, das einen 60-Sekunden-Einmalcode prägt und an den Callback des Clients umleitet. Die gesamte Autorisierungsanfrage überquert diese Seite in einem versteckten Feld, das mit MCP_TOKEN_SIGNING_KEY signiert ist, sodass ein Instanz-Austausch während der Einwilligung den Ablauf nicht bricht und das Formular nicht bearbeitet werden kann; ein Feld, das die Verifikation nicht besteht, ist eine 400 ohne Umleitung und ohne geprägten Code.

Beide Einwilligungsantworten tragen Cache-Control: no-store, Referrer-Policy: no-referrer – das Formular postet von der /authorize-URL, die state und die PKCE-Challenge in ihrer Query-String hat – X-Frame-Options: DENY und eine CSP von default-src 'none'; style-src 'unsafe-inline'; frame-ancestors 'none'; base-uri 'none'. Bewusst gibt es kein form-action: Browser waren sich uneinig, ob es gegen ein Umleitungsziel geprüft wird, und die Einwilligungs-POST-Antwort antwortet mit einer Umleitung zu claude.ai.

POST /consent hat ein eigenes Ratenlimit – 200 in 15 Minuten –, weil es absichtlich vor dem /authorize-Limiter des SDKs montiert ist und ein gerendertes Formular zehn Minuten lang postbar bleibt, sodass ein Durchlauf durch /authorize ein Feld ergibt, das wiedergegeben werden kann. Das Limit liegt über der 100-Einträge-Obergrenze für ausstehende Codes, sodass die eigene Verdrängung des Speichers, deren Verhalten spezifiziert ist, das Erste ist, worauf ein Burst trifft.

POST /token stellt ein Zugriffstoken aus, das eine Stunde gültig ist, und ein Refresh-Token, das 30 Tage gültig ist. Beide sind ein HMAC-SHA256 über eine kompakte Payload unter MCP_TOKEN_SIGNING_KEY und sonst nichts – kein Speicher wird zur Verifikation herangezogen, was eine Cloud-Run-Revisionsablösung davon abhält, eine Neuverbindung zu erzwingen. Die Payload trägt die Audience, nämlich MCP_PUBLIC_URL plus /mcp, sodass ein Token für diesen MCP-Endpunkt und keinen anderen gilt. Wie lange diese Tokens leben und wie man einen Menschen dazu bringt, öfter zuzustimmen, steht zwei Abschnitte weiter unten.

Bearer-Tokens auf dem MCP-Endpunkt

Jede Anfrage an /mcp benötigt Authorization: Bearer <access token>. Die requireBearerAuth des SDKs sitzt in createApp vor der MCP-Router, und verifyAccessToken in src/oauth-provider.ts ist das, was sie aufruft: die HMAC-Signatur unter MCP_TOKEN_SIGNING_KEY, die Token-Art, das Ablaufdatum und die Audience. Ein Token, das korrekt signiert und nicht abgelaufen ist, aber die Ressourcenkennung eines anderen Servers trägt, wird abgelehnt – das SDK prüft keine eigene Audience, sodass ohne diese Prüfung ein Token, das von einem Server mit diesem Signaturschlüssel für einen anderen MCP-Server geprägt wurde, akzeptiert würde.

Eine Anfrage ohne Token, mit abgelaufenem Token oder mit einem Token, das eine dieser Prüfungen nicht besteht, ist 401 mit einem Challenge-Header:

WWW-Authenticate: Bearer error="invalid_token", error_description="…",
                  resource_metadata="https://<MCP_PUBLIC_URL>/.well-known/oauth-protected-resource/mcp"

Der resource_metadata-Parameter ist der Teil, der zählt: Über ihn findet Claude den Autorisierungsserver und startet den Ablauf, sodass eine 401 ohne ihn eine Sackgasse statt einer Anmeldeaufforderung ist. Claude aktualisiert reaktiv bei einer 401 und proaktiv ein paar Minuten vor dem gespeicherten Ablaufdatum, sodass eine 401 hier ein gewöhnliches Ereignis ist.

Das Token wird aus dem Authorization-Header gelesen und von nirgendwo sonst. ?access_token= in der Query-String wird nicht beachtet – die MCP-Autorisierungsspezifikation verbietet es, und src/logging.ts lässt die Query-String im Vertrauen darauf aus der Logzeile.

Welche Routen offen sind, ist die Ausnahmeliste, und sie ist länger als „alles außer /mcp", weil der gesamte Autorisierungsablauf Anrufer beantworten muss, die noch kein Token halten: /healthz und /health, beide .well-known-Dokumente, /authorize, /consent und /token. Ein Test in test/server.test.ts zählt die Routen auf, die createApp tatsächlich registriert, und behauptet, dass jede nicht auf dieser Liste ohne Token mit 401 antwortet, sodass eine später hinzugefügte Route geschlossen ist, sofern sie nicht absichtlich geöffnet wird.

Es sind keine Scopes erforderlich. offline_access, der eine Scope, den dieser Server ausstellt, betrifft, ob ein Refresh-Token gewährt wird, und nicht, was ein Aufrufer tun darf, und ihn zu verlangen würde für Tokens, die sonst gültig sind, mit 403 antworten. Falls je eine Scope-Prüfung hinzugefügt wird, muss die 403 WWW-Authenticate: Bearer error="insufficient_scope" tragen – was diese Middleware tut –, weil Claude jede andere 403 als endgültig behandelt und zu nichts auffordert.

Token-Lebensdauer und Erzwingen erneuter Validierung

Dieser Server ist für den unbeaufsichtigten Betrieb gebaut. Die Standardeinstellungen spiegeln das wider, und sie geben etwas von der Fähigkeit auf, ein durchgesickertes Credential abzuschneiden. Lesen Sie dies, bevor Sie ihn irgendwo einsetzen, wo es darauf ankommt, und ändern Sie die Zahlen, wenn der Tausch für Sie falsch ist.

Was die Standardeinstellungen tun

Token

Lebensdauer

Was es erneuert

Access-Token

1 Stunde

Das Refresh-Token, automatisch

Refresh-Token

30 Tage

Jede Erneuerung erzeugt ein neues mit frischen 30 Tagen

Zustimmungsformular

10 Minuten

Nichts; ein abgelaufenes Formular wird abgelehnt und der Ablauf startet neu

Claude erneuert von selbst – proaktiv, bevor die Stunde um ist, und reaktiv bei einem 401. Ein Mensch klickt also auf „Approve“, wenn der Connector zum ersten Mal hinzugefügt wird, und danach nur, wenn der Connector 30 Tage lang ungenutzt bleibt. Das ist das gleitende Fenster: Die 30 Tage begrenzen, wie lange die Verbindung inaktiv bleiben darf, nicht wie lange sie leben darf.

Warum gleitend, und was es kostet

Jedes Token, das dieser Server ausstellt, ist zustandslos. Es ist eine signierte Nutzlast und sonst nichts – keine Datenbankzeile, kein Sitzungsdatensatz, nichts, was beim Zurückkommen nachgeschlagen werden müsste. Genau das macht einen Cloud-Run-Revisionsaustausch unsichtbar: Die neue Instanz verifiziert ein Token, das die alte Instanz ausgestellt hat, ohne gemeinsamen Zustand zwischen ihnen. Ein Token-Speicher würde bei jedem Deployment eine erneute Verbindung bedeuten.

Der Preis dafür ist, dass nichts einzeln widerrufen werden kann. Es gibt keinen Widerrufs-Endpunkt, weil es nichts zu löschen gäbe. Im Einzelnen:

  • Ein durchgesickertes Refresh-Token gewährt bis zu 30 Tage lang Zugriff, und jede Nutzung verlängert den Zugriff seines Inhabers um weitere 30. Es gibt keinen serverseitigen Datensatz, der ungültig gemacht werden könnte, und keine Möglichkeit, ein gestohlenes Refresh-Token von einem legitimen zu unterscheiden – beide sind dieselben Bytes, die mit demselben Schlüssel signiert wurden.

  • Das Gleiten des Fensters ist keine Rotation. Wenn eine Erneuerung ein neues Refresh-Token erzeugt, funktioniert das ersetzte weiter, bis die darin eingestempelte Ablaufzeit erreicht ist. Echte Rotation bedeutet, das alte Token als verbraucht zu markieren, was den Speicher erfordert, den dieses Design nicht hat.

  • Ein Access-Token kann innerhalb seiner Stunde aus demselben Grund nicht gesperrt werden.

Übrig bleibt ein einziges grobes Hebelmittel, und es wirkt sofort: Ändern Sie MCP_TOKEN_SIGNING_KEY und stellen Sie erneut bereit. Jedes Access-Token, jedes Refresh-Token und jede offene Zustimmungsseite wird auf einmal ungültig, weil alle gegen diesen Schlüssel verifiziert werden. Die nächste Claude-Anfrage erhält eine 401 und der Betreiber klickt einmal auf „Approve“. Den Schlüssel planmäßig zu rotieren ist für sich genommen eine vernünftige Richtlinie.

Der Zustimmungsbildschirm authentifiziert, so viel sei gesagt, niemanden – er hat einen Button und kein Passwort. Was zwischen einem Fremden und Ihren Notebooks steht, ist MCP_OAUTH_CLIENT_SECRET, das POST /token verlangt, die Redirect-URI-Allowlist, die jeden Autorisierungscode an claude.ai oder an Loopback sendet, und PKCE, das den Code an den Client bindet, der den Ablauf gestartet hat.

Menschen öfter zustimmen lassen

Jede dieser Änderungen ist eine Quellcode-Änderung, kein Konfigurationswert. Das ist beabsichtigt: Ein Betreiber, der das Fenster verkürzt, verändert das Sicherheitsprofil des Deployments, und das gehört in einen Commit, den jemand lesen kann, und nicht in eine Umgebungsvariable, die jemand vergessen kann.

Verkürzen Sie das Inaktivitätsfenster. In src/oauth-provider.ts:

const REFRESH_TOKEN_TTL_S = 30 * 24 * 60 * 60;   // 30 days
const REFRESH_TOKEN_TTL_S = 7 * 24 * 60 * 60;    // a week

So lange ungenutzt, braucht der Connector einen Klick. Bei regelmäßiger Nutzung fragt er weiterhin nie – das Fenster gleitet immer weiter. Das begrenzt, wie lange ein durchgesickertes Refresh-Token überlebt, nachdem das Leck nicht mehr genutzt wird, und sonst nichts.

Stoppen Sie das Gleiten des Fensters. Das war ursprünglich in Issue #22 festgelegt, und es begrenzt die Gesamtlebensdauer einer Verbindung statt ihrer Inaktivitätszeit: Ein Mensch genehmigt alle 30 Tage, egal wie beschäftigt der Connector ist. Eine Zeile in exchangeRefreshToken, in src/oauth-provider.ts:

// Sliding: a new refresh token, 30 days from now.
return issueTokens(client.client_id, requested, mintRefreshToken(client.client_id, granted));

// Fixed: hand back the same token, expiring 30 days after the consent click.
return issueTokens(client.client_id, requested, refreshToken);

Verweigern Sie die Ausstellung von Refresh-Tokens ganz. Die strengste Einstellung: Ein Mensch genehmigt jede Stunde, weil ein abgelaufenes Access-Token nichts hat, das es erneuert. Zwei Änderungen, beide nötig – der Metadaten-Schalter allein verhindert die Ausstellung des Tokens nicht.

  1. Leeren Sie in src/oauth-router.ts SCOPES_SUPPORTED. Claude hängt offline_access nur dann an eine Autorisierungsanfrage an, wenn die Metadaten es ankündigen, und das ist der Schalter, der entscheidet, ob ein Refresh-Token angefordert wird.

  2. Entfernen Sie in src/oauth-provider.ts das Feld refresh_token aus dem, was issueTokens zurückgibt. Es wird heute unabhängig von den angeforderten Scopes ausgestellt.

Erwarten Sie, dass diese Änderung in der Nutzung sichtbar ist: Claude schickt den Browser mitten in der Sitzung zurück zum Zustimmungsbildschirm, wenn die Stunde abläuft.

Verkürzen Sie das Access-Token. ACCESS_TOKEN_TTL_S in src/oauth-provider.ts verengt das Fenster, in dem ein durchgesickertes Access-Token funktioniert. Es kostet eine Token-Anfrage pro Ablauf und keinerlei menschliches Eingreifen, ist also billig – aber es bewirkt nichts gegen ein durchgesickertes Refresh-Token, das die Sorge wert ist.

Keepalive

Die delegierten Refresh-Tokens von Microsoft verfallen nach etwa 90 Tagen ohne Nutzung. Das Token gleitet nur dann weiter, wenn es tatsächlich ausgetauscht wird, und es wird nur ausgetauscht, wenn ein Tool-Aufruf eintrifft, nachdem das gehaltene Access-Token abgelaufen ist – ein Connector, den niemand drei Monate lang nutzt, ist also ein Connector, der eine Person an einem Browser braucht, die npm run bootstrap ausführt. Nichts im Server kann das von sich aus verhindern, weil nichts im Server läuft, wenn niemand ihn aufruft.

POST /keepalive ist die Lösung. Es ruft acquireTokenSilent mit forceRefresh: true auf, was das gehaltene Access-Token überspringt und das Refresh-Token austauscht, sodass Entra ein Ersatz-Token mit einem frischen Fenster ausstellt und src/token-cache.ts es in Firestore schreibt. forceRefresh ist der tragende Teil: Ohne es antwortet MSAL aus seinem eigenen Cache, keine Anfrage erreicht Entra, und das Fenster bewegt sich nicht.

Setzen Sie MCP_KEEPALIVE_SECRET auf mindestens 32 zufällige Zeichen, und die Route wird eingebunden; lassen Sie es ungesetzt, und der Pfad antwortet mit 404. Ein Scheduler präsentiert das Geheimnis im X-Keepalive-Secret-Header, der in konstanter Zeit verglichen wird, bevor irgendetwas ausgeführt wird. Es ist ein gemeinsames Geheimnis und kein Bearer-Token, weil ein Scheduler den OAuth-Ablauf nicht ausführen kann – er hat keinen Browser und keinen Ort, um ein Refresh-Token aufzubewahren – und es ist eine eigene Variable und nicht das Layer-1-Client-Secret, damit eine Berechtigung, die die gesamte MCP-Oberfläche erreichen kann, nicht auch in einem Scheduler-Job sitzt.

gcloud scheduler jobs create http onenote-mcp-keepalive \
  --schedule="0 4 * * 1" \
  --time-zone=UTC \
  --uri="https://YOUR-SERVICE-URL/keepalive" \
  --http-method=POST \
  --headers="X-Keepalive-Secret=YOUR-SECRET" \
  --attempt-deadline=60s \
  --max-retry-attempts=3

Wöchentlich ist reichlich für ein 90-Tage-Fenster und lässt Raum für mehrere verpasste Läufe. Der Job kostet einen Roundtrip zum Token-Endpunkt und einen Firestore-Schreibvorgang.

Status

Bedeutung

Was der Scheduler tun sollte

200

Das Refresh-Token wurde ausgetauscht und das neue gespeichert

Nichts

401

Das Geheimnis fehlt oder ist falsch

Job korrigieren; die Route hat nichts getan

404

MCP_KEEPALIVE_SECRET ist im Dienst nicht gesetzt

Setzen und erneut bereitstellen

503 mit "retryable": true

Firestore war nicht erreichbar

Erneut versuchen

503 mit "retryable": false

Die Berechtigung ist tot

npm run bootstrap

Wovor das nicht schützt: eine Richtlinie für die Anmeldehäufigkeit beim bedingten Zugriff, eine Passwortänderung, ein MFA-Reset oder ein Administrator, der die Berechtigung widerruft. Jede dieser Maßnahmen tötet das Refresh-Token, egal was der Zeitplan sagt, und keine Codeänderung verhindert das. Wenn der Entra-Mandant Ihnen gehört, nehmen Sie diese App-Registrierung von den Richtlinien zur Anmeldehäufigkeit aus; wenn nicht, behandeln Sie 90 Tage als Obergrenze, die jemand anderes verkürzen kann, ohne es Ihnen zu sagen.

Die Keepalive-Route hat auch nichts mit dem 30-Tage-Fenster von Layer-1 in Token-Lebensdauer unten zu tun. Dieses Refresh-Token lebt im Connector-Speicher von Claude, und nur Claude kann es vorlegen oder seinen Ersatz empfangen, sodass nichts, was hier läuft, es am Leben halten kann. Es zu verlieren kostet einen Klick auf den Approve-Button; das Microsoft-Token zu verlieren kostet eine Device-Code-Anmeldung.

Alarmierung

Zwei Fehler sind ohne eine auf Logs basierende Metrik unsichtbar, weil beide nur als Nachricht in einer Claude-Unterhaltung oder als Zeile erscheinen, die niemand liest:

Ereignis

Bedeutung

graph-auth-failure mit retryable: "false"

Die Microsoft-Berechtigung ist tot. Jemand muss npm run bootstrap ausführen.

token-cache-write-refused

MSAL hat einen Cache ohne Anmeldedaten übergeben. Die gespeicherte Kopie hat überlebt; etwas ist falsch.

gcloud logging metrics create onenote_mcp_auth_failure \
  --description="Microsoft Graph credential failures needing an operator" \
  --log-filter='resource.type="cloud_run_revision"
    resource.labels.service_name="onenote-mcp"
    jsonPayload.event=("graph-auth-failure" OR "token-cache-write-refused")
    jsonPayload.retryable!="true"'

Dann eine Alarmrichtlinie, wenn diese Metrik über null liegt. Auch eine Zustimmungsgenehmigung ist beobachtenswert: POST /consent, das mit 302 antwortet, sollte nur passieren, wenn Sie den Connector hinzufügen, und das Anfrageprotokoll enthält es bereits.

jsonPayload.event="request" jsonPayload.path="/consent" jsonPayload.status=302

Bootstrap

npm run bootstrap ist die einzige interaktive Microsoft-Anmeldung im Projekt, und sie läuft auf Ihrem Rechner, nicht auf Cloud Run. Sie meldet sich mit dem Device-Code-Ablauf an und schreibt den resultierenden MSAL-Cache in das Firestore-Dokument, das der Server liest.

gcloud auth application-default login

ONENOTE_CLIENT_ID=00000000-0000-0000-0000-000000000000 \
ONENOTE_AUTHORITY=https://login.microsoftonline.com/common \
GOOGLE_CLOUD_PROJECT=your-project \
FIRESTORE_CACHE_DOC=tokencache/msal \
npm run bootstrap

Es gibt Microsofts Device-Code-Nachricht aus, wartet darauf, dass Sie in einem Browser zustimmen, listet dann einmal Ihre Notebooks auf und gibt die Anzahl als Beweis aus, dass das Token funktioniert. Die abschließenden Zeilen nennen das Firestore-Projekt und das geschriebene Dokument sowie den Home-Tenant des Kontos, damit Sie sehen können, dass Sie sich im richtigen Verzeichnis angemeldet haben. Diese Ausgabe enthält die Tenant-ID; halten Sie sie von Issues, Pull Requests und Workflow-Logs fern.

GOOGLE_CLOUD_PROJECT und FIRESTORE_CACHE_DOC sind hier erforderlich, anders als auf dem Server, wo Ersteres abgeleitet und Letzteres standardmäßig gesetzt wird. Die CLI schreibt mit Ihren eigenen Application Default Credentials, sodass ein nicht gesetzter Wert ein echtes Dokument in dem Projekt anlegen würde, auf das Ihr gcloud-Login zeigt, und trotzdem eine Erfolgszeile ausgeben würde. Die MCP_OAUTH_*-Werte werden nicht gelesen, daher landet das Layer-1-Client-Secret bei diesem Lauf nie auf Ihrem Rechner.

Führen Sie es erneut aus, wenn ein graph-auth-failure-Ereignis mit retryable: "false" in den Server-Logs erscheint. Das Refresh-Token wird bei jeder Nutzung rotiert und stirbt, wenn der Dienst etwa 90 Tage lang inaktiv bleibt; es gibt keine automatische Wiederherstellung. Das Konfigurieren des Keepalive-Jobs oben verhindert, dass Inaktivität einer der Wege dorthin ist.

Container

Der Dienst wird auf Cloud Run bereitgestellt, das linux/amd64 ausführt. Das Image wird ausdrücklich für diese Plattform gebaut, damit die native Binärdatei von @resvg/resvg-js passt. Die Laufzeitbasis ist Debian node:24-slim und darf nicht zu Alpine werden – das resvg-Prebuild ist nur für glibc.

docker build --platform linux/amd64 -t onenote-mcp .

docker run --rm -p 8080:8080 \
  -e PORT=8080 \
  -e ONENOTE_CLIENT_ID=00000000-0000-0000-0000-000000000000 \
  -e ONENOTE_AUTHORITY=https://login.microsoftonline.com/common \
  -e MCP_OAUTH_CLIENT_ID=test-client \
  -e MCP_OAUTH_CLIENT_SECRET=test-secret \
  -e MCP_TOKEN_SIGNING_KEY=0123456789abcdef0123456789abcdef \
  -e MCP_PUBLIC_URL=https://onenote-mcp.example.run.app \
  onenote-mcp

curl -i localhost:8080/health     # 200, {"status":"ok",...}

/healthz antwortet mit demselben und wird von Cloud Runs eigenen Probes verwendet. Rufen Sie es nicht von außen auf: Googles Frontend antwortet auf https://<service>.run.app/healthz mit einer eigenen 404-Seite, und die Anfrage erreicht den Container nie. Eine externe Uptime-Prüfung muss daher /health verwenden. Gemessen am bereitgestellten Dienst am 2026-08-19 – /health, /healthz2 und sogar /Healthz kommen alle an, und nur das exakte kleingeschriebene /healthz wird verschluckt.

Diese Werte sind Platzhalter, die nur gut genug geformt sind, um die Startvalidierung zu bestehen; sie authentifizieren nichts. Auf Cloud Run wird PORT von der Plattform bereitgestellt und der Rest kommt aus dem Deploy-Workflow. Wenn der Host-Port 8080 bereits belegt ist, mappen Sie einen anderen: -p 8081:8080 mit -e PORT=8080.

So testen Sie das Image:

RUN_DOCKER_TESTS=1 bash scripts/test/run.sh

Das baut das Image, prüft, dass die resvg-glibc-Binärdatei die Produktionsinstallation überlebt hat und keine Dev-Abhängigkeiten mitgekommen sind, rendert ein SVG zu PNG im Container und stellt sicher, dass /healthz auf dem in PORT angegebenen Port mit 200 antwortet. Ohne RUN_DOCKER_TESTS=1 wird die Docker-Suite übersprungen und der Rest läuft trotzdem.

Deployment

.github/workflows/deploy.yml wird bei jedem Push auf main und bei workflow_dispatch ausgeführt. Es führt Typprüfung durch, führt npm test aus, baut, baut und pusht das Container-Image in die Artifact Registry, getaggt mit dem Commit-SHA, und stellt dieses Image in Cloud Run bereit. Ein fehlgeschlagener Typprüfung oder Test stoppt den Lauf, bevor ein Image gebaut wird.

Es gibt keine langlebigen Anmeldeinformationen in GitHub. Der Job authentifiziert sich über Workload Identity Federation: permissions: id-token: write ermöglicht es, ein GitHub-OIDC-Token anzufordern, und google-github-actions/auth@v2 tauscht dieses gegen kurzlebige Google-Anmeldeinformationen. Es wird kein Service-Account-JSON-Schlüssel von scripts/gcp-bootstrap.sh erstellt oder irgendwo benötigt. Der Provider akzeptiert nur Token, deren repository-Anspruch dieses Repository ist.

Das Image wird auf ubuntu-latest gebaut, was linux/amd64 ist – die Plattform, auf der Cloud Run läuft, und die, für die das @resvg/resvg-js-Prebuild kompiliert ist. Deshalb baut der Workflow das Image selbst, anstatt gcloud run deploy --source zu verwenden, was auch bedeuten würde, Cloud Build zu aktivieren und die damit verbundenen Rollen zu gewähren.

Der Deploy läuft mit --max-instances=1 und --allow-unauthenticated als Laufzeit-Servicekonto, das roles/datastore.user für den Firestore-Token-Cache besitzt. --allow-unauthenticated ist es, das Claude überhaupt den Zugriff auf den Dienst ermöglicht; der MCP-Endpunkt ist stattdessen durch das Bearer-Token geschlossen. Siehe Bearer-Token auf dem MCP-Endpunkt.

Was das Repository enthalten muss

scripts/gcp-bootstrap.sh richtet die GCP-Seite ein und gibt die gh variable set-Befehle für die ersten sechs aus. Der Workflow schlägt in seinem ersten Schritt fehl und benennt, was fehlt, anstatt halb konfiguriert bereitzustellen.

Name

Art

Wert

GCP_PROJECT

Variable

Projekt-ID

GCP_REGION

Variable

Cloud-Run-Region

GAR_REGION

Variable

Artifact-Registry-Region

WIF_PROVIDER

Variable

Vollständiger Ressourcenname des Workload-Identity-Providers

DEPLOY_SA

Variable

E-Mail des Deploy-Servicekontos

RUNTIME_SA

Variable

E-Mail des Laufzeit-Servicekontos

ONENOTE_CLIENT_ID

Variable

Client-ID der Azure-App-Registrierung

ONENOTE_AUTHORITY

Variable

Entra-Autoritäts-URL

MCP_OAUTH_CLIENT_ID

Variable

OAuth-Client-ID der Schicht 1

MCP_PUBLIC_URL

Variable

Die öffentliche URL des Dienstes; siehe unten

FIRESTORE_CACHE_DOC

Variable

Optional; Standard: tokencache/msal

MCP_OAUTH_CLIENT_SECRET

Geheimnis

OAuth-Client-Geheimnis der Schicht 1

MCP_TOKEN_SIGNING_KEY

Geheimnis

Signaturschlüssel für Zugriffstoken, mindestens 32 Zeichen

MCP_KEEPALIVE_SECRET

Geheimnis

Optional; nicht gesetzt bedeutet, dass POST /keepalive nicht bereitgestellt wird

Nur drei davon sind Anmeldeinformationen. Der WIF-Providername und die E-Mail-Adressen der Servicekonten sind Identifikatoren, die für jeden nutzlos sind, der nicht die OIDC-Identität dieses Repositorys vorweisen kann, also sind sie Variablen und keine Geheimnisse.

Der Deploy übergibt env_vars_update_strategy: overwrite, sodass die Liste im Workflow bei jeder Revision die gesamte Umgebung des Dienstes ist. Die Standardeinstellung der Aktion ist merge, bei der eine aus dem Workflow entfernte Variable stillschweigend aus der vorherigen Revision überleben würde. PORT und GOOGLE_CLOUD_PROJECT sind bewusst nicht in der Liste: Cloud Run stellt beide bereit und lehnt PORT als Eingabe ab.

Der erste Deploy und MCP_PUBLIC_URL

MCP_PUBLIC_URL ist der OAuth-Aussteller und die Zielgruppe (Audience) jedes Zugriffstokens, das dieser Server ausstellt, und es existiert kein Dienst, der eine URL hat, bis der erste Deploy stattgefunden hat. Der Workflow löst es in drei Schritten auf: die Repository-Variable MCP_PUBLIC_URL, dann die URL, die Cloud Run dem Dienst bereits zugewiesen hat, dann – nur wenn keines existiert – https://placeholder.invalid, das er unmittelbar nach dem Deploy durch die echte URL ersetzt. Ein erster Lauf funktioniert also unbeaufsichtigt und endet mit dem korrekten Wert. Er hinterlässt eine Warnung, die die URL nennt; setzen Sie die Repository-Variable darauf, denn das ist die einzige der drei Quellen, die das Anbringen einer benutzerdefinierten Domain vor dem Dienst überlebt.

Das Ändern von MCP_PUBLIC_URL macht an sich nichts ungültig, aber jedes bereits ausgestellte Zugriffstoken ist an die alte Zielgruppe gebunden und wird abgelehnt. Claude führt in diesem Fall den Autorisierungsablauf erneut aus.

Rollback

Das Image-Tag ist der Commit-SHA, sodass ein früheres Image noch in der Artifact Registry ist:

gcloud run services update-traffic onenote-mcp --region "$GCP_REGION" --to-revisions <revision>=100

Das erneute Ausführen des Workflows von einem früheren Commit mit workflow_dispatch funktioniert ebenfalls und ist das, was die bereitgestellte Umgebung mit der Workflow-Datei dieses Commits in Einklang hält.

Konfiguration

Jeder Wert stammt aus einer Umgebungsvariable, die beim Start validiert wird. Eine fehlende oder fehlerhafte Variable erzeugt einen ConfigError, der alles auflistet, was falsch ist, und der Prozess beendet sich mit Exit-Code 1 ohne Stacktrace.

Variable

Erforderlich

Standard

Zweck

ONENOTE_CLIENT_ID

ja

Client-ID der Azure-App-Registrierung (öffentlicher Client)

ONENOTE_AUTHORITY

ja

Entra-ID-Autoritäts-URL für den Mandanten

MCP_OAUTH_CLIENT_ID

ja

OAuth-Client-ID der Schicht 1, die Claude präsentiert

MCP_OAUTH_CLIENT_SECRET

ja

OAuth-Client-Geheimnis der Schicht 1

MCP_TOKEN_SIGNING_KEY

ja

Schlüssel zum Signieren ausgestellter Zugriffstoken (mind. 32 Zeichen)

MCP_PUBLIC_URL

ja

Die eigene öffentliche URL des Dienstes: https, keine Abfrage, kein Fragment, kein abschließender Schrägstrich

FIRESTORE_CACHE_DOC

Server: nein · Bootstrap: ja

tokencache/msal

Firestore-Dokumentpfad, der den MSAL-Token-Cache enthält

GOOGLE_CLOUD_PROJECT

Server: nein · Bootstrap: ja

GCP-Projekt; wird auf Cloud Run automatisch abgeleitet

PORT

nein

8080

Bind-Port. Cloud Run setzt diesen; der Server kodiert nie einen fest.

MCP_KEEPALIVE_SECRET

nein

Mindestens 32 Zeichen. Setzen Sie es und POST /keepalive wird bereitgestellt; lassen Sie es ungesetzt und der Pfad gibt 404 zurück. Siehe Keepalive.

FIRESTORE_CACHE_DOC benennt das Dokument, das das MSAL-Cache-Plugin in src/token-cache.ts liest und schreibt. Sein Wert muss ein Dokumentpfad sein, also eine gerade Anzahl von durch Schrägstriche getrennten Segmenten; loadConfig lehnt einen Sammlungspfad beim Start ab.

ONENOTE_CLIENT_ID und ONENOTE_AUTHORITY identifizieren die Azure-App-Registrierung, die src/graph-auth.ts gegenüber Entra ID präsentiert. Es ist ein öffentlicher Client, daher gibt es bewusst kein Client-Geheimnis der Schicht 2; die MCP_OAUTH_*-Werte darunter gehören zu Schicht 1, zwischen Claude und diesem Server, und sind nicht verwandt.

MCP_PUBLIC_URL ist die URL, unter der Claude diesen Dienst erreicht. Der OAuth-Aussteller, die resource-Kennung, an die ein Token gebunden ist, und die URL des Metadaten-Dokuments der geschützten Ressource werden alle daraus abgeleitet. Nichts auf Cloud Run teilt dem Prozess mit, unter welcher URL er erreicht wird, und ein aus dem Host-Header entnommener Wert wäre das, was der Aufrufer gesendet hat, also wird es konfiguriert. Es kann erst ausgefüllt werden, nachdem der erste Deploy die URL erzeugt hat.

npm run bootstrap liest nur ONENOTE_CLIENT_ID, ONENOTE_AUTHORITY, FIRESTORE_CACHE_DOC und GOOGLE_CLOUD_PROJECT – nicht die MCP_OAUTH_*-Werte – und es verlangt die letzten beiden, anstatt sie zu standardisieren. Siehe Bootstrap.

Repository-Hygiene

Dieses Repository ist öffentlich. Committen Sie keine echten Seiteninhalte, gerenderten Text, Entra-Mandantennamen oder -IDs oder Firestore-Dokumentinhalte. .gitignore schließt output/ und die Token-Cache-Dateimuster aus; siehe den Abschnitt „Repo-Hygiene“ in project-spec.md.

A
license - permissive license
Not graded
quality - not tested
B
maintenance

Maintenance

Maintainers
7hResponse time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    Enables interaction with Microsoft OneNote via the Microsoft Graph API, allowing users to list notebooks and retrieve page content. It supports both personal and organization notebooks with credential caching for efficient authentication.
    15
    3
    MIT
  • A
    license
    D
    quality
    C
    maintenance
    Enables AI assistants to securely interact with Microsoft OneNote data through the Microsoft Graph API. It supports comprehensive management tasks including searching page content, creating and editing notes, and automating productivity workflows like daily note creation.
    20
    2
    MIT

View all related MCP servers

Related MCP Connectors

  • Microsoft OneNote (Microsoft 365) MCP Pack

  • MCP-native open-source Notion alternative: read & write pages, databases and kanban boards.

  • Access the Notra API for managing posts, brand identities, integrations, and schedules.

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/dovrosenberg/onenote-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server