onenote-mcp
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 testSkripte
Skript | Was es tut |
| Kompiliert |
| Typprüfung ohne Ausgabe |
| Führt den Server aus dem Quellcode mit |
| Führt den kompilierten Server aus |
| Lokale Gerätecode-Anmeldung, die den Firestore-Token-Cache befüllt |
|
|
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-pitrEin 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-emulatorgcloud 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:
| Was passiert ist | Was zu tun ist |
| Das Firestore-Dokument fehlt, oder sein |
|
| Firestore hat nicht geantwortet, oder das Laufzeit-Dienstkonto hat | Wiederholen. Keine Anmeldung. |
| Der Cache wurde gelesen, enthält aber kein angemeldetes Konto |
|
| Das gespeicherte Refresh-Token ist abgelaufen oder widerrufen, oder der Token-Endpunkt hat nichts Brauchbares zurückgegeben |
|
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 |
| Jedes Notizbuch, nach Anzeigename |
| Abschnitte direkt unter einem Notizbuch oder einer Abschnittsgruppe |
| Abschnittsgruppen direkt unter einem Notizbuch oder einer Abschnittsgruppe |
| Beide oben genannten, zusammen abgerufen |
| Seiten in einem Abschnitt, zuletzt geändert zuerst, höchstens |
| Ein Notizbuch mit jeder verschachtelten Abschnittsgruppe aufgelöst |
| Jedes Notizbuch, jeweils mit aufgelöstem Baum |
| Jedes Notizbuch mit seinen Abschnitten und einer Ebene Abschnittsgruppe, in einer einzigen Anfrage |
| Abschnitte überall im Konto, deren Name diesen Text enthält, jeweils mit übergeordnetem Notizbuch und Abschnittsgruppe |
| 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.
getNotebookTreerekursiert.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.listPagesInSectionstoppt, sobaldtopElemente vorliegen, daher isttopeine Ergebnisanzahl und keine Seitengröße.Die kontoweite Seitenliste wird nie aufgerufen.
GET /me/onenote/pagesschlägt mit Fehler 20266, "maximum sections exceeded", bei einer Notizbuch-pro-Jahr-Struktur fehl. Die Seitenauflistung ist immer auf/me/onenote/sections/{id}/pagesbeschränkt, und ein Test durchsuchtsrc/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 |
|
|
|
|
|
|
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-parserist mitremoveNSPrefix: truekonfiguriert, 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 |
| RFC-8414-Autorisierungsserver-Metadaten |
| RFC-9728-Metadaten der geschützten Ressource |
| Autorisierungs-Endpunkt |
| Wohin das Einwilligungsformular postet; die |
| Token-Endpunkt |
curl -s localhost:8080/.well-known/oauth-authorization-server
curl -s localhost:8080/.well-known/oauth-protected-resource/mcpAlles 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 weekSo 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.
Leeren Sie in
src/oauth-router.tsSCOPES_SUPPORTED. Claude hängtoffline_accessnur dann an eine Autorisierungsanfrage an, wenn die Metadaten es ankündigen, und das ist der Schalter, der entscheidet, ob ein Refresh-Token angefordert wird.Entfernen Sie in
src/oauth-provider.tsdas Feldrefresh_tokenaus dem, wasissueTokenszurü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=3Wö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 |
| Setzen und erneut bereitstellen |
503 mit | Firestore war nicht erreichbar | Erneut versuchen |
503 mit | Die Berechtigung ist tot |
|
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 |
| Die Microsoft-Berechtigung ist tot. Jemand muss |
| 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=302Bootstrap
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 bootstrapEs 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.shDas 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 |
| Variable | Projekt-ID |
| Variable | Cloud-Run-Region |
| Variable | Artifact-Registry-Region |
| Variable | Vollständiger Ressourcenname des Workload-Identity-Providers |
| Variable | E-Mail des Deploy-Servicekontos |
| Variable | E-Mail des Laufzeit-Servicekontos |
| Variable | Client-ID der Azure-App-Registrierung |
| Variable | Entra-Autoritäts-URL |
| Variable | OAuth-Client-ID der Schicht 1 |
| Variable | Die öffentliche URL des Dienstes; siehe unten |
| Variable | Optional; Standard: |
| Geheimnis | OAuth-Client-Geheimnis der Schicht 1 |
| Geheimnis | Signaturschlüssel für Zugriffstoken, mindestens 32 Zeichen |
| Geheimnis | Optional; nicht gesetzt bedeutet, dass |
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>=100Das 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 |
| ja | — | Client-ID der Azure-App-Registrierung (öffentlicher Client) |
| ja | — | Entra-ID-Autoritäts-URL für den Mandanten |
| ja | — | OAuth-Client-ID der Schicht 1, die Claude präsentiert |
| ja | — | OAuth-Client-Geheimnis der Schicht 1 |
| ja | — | Schlüssel zum Signieren ausgestellter Zugriffstoken (mind. 32 Zeichen) |
| ja | — | Die eigene öffentliche URL des Dienstes: |
| Server: nein · Bootstrap: ja |
| Firestore-Dokumentpfad, der den MSAL-Token-Cache enthält |
| Server: nein · Bootstrap: ja | — | GCP-Projekt; wird auf Cloud Run automatisch abgeleitet |
| nein |
| Bind-Port. Cloud Run setzt diesen; der Server kodiert nie einen fest. |
| nein | — | Mindestens 32 Zeichen. Setzen Sie es und |
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.
This server cannot be installed
Maintenance
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
- AlicenseNot gradedqualityDmaintenanceEnables AI language models to interact with Microsoft OneNote via a standardized interface, supporting notebook and page management through natural language.1527MIT
- AlicenseNot gradedqualityDmaintenanceEnables 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.153MIT
- AlicenseDqualityCmaintenanceEnables 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.202MIT
- AlicenseNot gradedqualityFmaintenanceEnables natural language access to Microsoft OneNote notebooks, sections, and pages for reading and listing content.45MIT
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.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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