onenote-mcp
onenote-mcp
Microsoft Graph を通じて Microsoft OneNote を公開する MCP サーバー — ノートブックと セクションの構造、ページコンテンツ、そして呼び出し元モデルが読める画像にレンダリングされた手書き文字。
project-spec.md が権威ある設計ドキュメントです。 インク再構築パイプライン、2つの独立したOAuthレイヤー、Cloud Run デプロイモデル、Firestore バックアップのトークンキャッシュをカバーしています。ここを変更する前に読んでください。
要件
Node >= 24。@google-cloud/firestore は Node >= 22 を必要とし、Node 24 が現在の Active LTS です。
Related MCP server: OneNoteMCP
クイックスタート
npm ci
npm run build
npm testスクリプト
スクリプト | 機能 |
|
|
| 出力なしで型チェック |
|
|
|
|
| Firestore トークンキャッシュをシードするローカルデバイスコードサインイン |
|
|
テストは test/ にあり、src/ をミラーリングしています。Node のネイティブ型ストリッピングを使用して TypeScript ソースに対して直接実行されるため、npm test はビルドを必要としません。これによりソースに制約が生じます: enum なし、namespace なし、コンストラクタパラメータプロパティなし、型のみのインポートは import type と書く必要があります。erasableSyntaxOnly と verbatimModuleSyntax コンパイラオプションがこれを強制します。
ディレクトリレイアウトとそれに伴う規約については CLAUDE.md を参照してください。
トークンキャッシュ
src/token-cache.ts は、単一の Firestore ドキュメントに対する MSAL の ICachePlugin を実装しており、そのパスは FIRESTORE_CACHE_DOC から取得されます。beforeCacheAccess はドキュメントの cache フィールドを読み取り、その文字列を MSAL に渡します。afterCacheAccess は、MSAL がキャッシュが変更されたと報告した場合にのみ、シリアライズされたキャッシュを Firestore トランザクション内で書き戻します。存在しないドキュメントは空のキャッシュとして読み取られ、これは npm run bootstrap が実行される前の状態です。両方のエントリポイントがこの同じプラグインを使用します: ブートストラップ CLI はそれを通じてキャッシュを書き込み、サーバーはそれを通じて読み取るため、シリアライザは1つで、同期を保つための2番目の形式はありません。
そのブロブがリフレッシュトークンの唯一のコピーであるため、2つのことがそれを保護します。
ドキュメントを空にする書き込みは拒否されます。 MSAL は一部の障害時にインメモリキャッシュから資格情報を削除し、afterCacheAccess は MSAL の finally ブロック内で実行されるため、アカウントを失ったシリアライゼーションがこのコードに到達する可能性がありますが、保存されているものはまだ有効です。overwriteWouldEmptyCache がそれを停止し、{"event":"token-cache-write-refused"} をログに記録します。空チェックは MSAL のキー名を読み取りません — キャッシュは、すべての値が空のコンテナであるオブジェクトに解析されるときに空です — そのため、MSAL が形式を変更しても反転できず、認識されないものはブロックされるのではなく通過が許可されます。
各書き込みが置き換えるブロブは previousCache フィールドに保持されます。 履歴ではなく1世代: キャッシュはリフレッシュのたびに書き換えられ、有用なコピーは常に最新の良好なものです。悪い書き込みからの回復は、Firestore コンソールでそのフィールドを cache にコピーすることであり、代替手段がデバイスコードサインインであるため、それだけの価値があります。ポイントインタイムリカバリを有効にして2番目のレイヤーを追加します:
gcloud firestore databases update --enable-pitrバックエンドの障害は資格情報の障害ではありません。 Firestore が到達不能であるか、roles/datastore.user バインディングが失効している場合、TokenCacheUnavailableError が発生し、期限切れのリフレッシュトークンが生成するエラーとして表面化しません。書き込みはその前に3回再試行されます。この区別がコードの価値がある理由については、以下の表の cache-unavailable 行を参照してください。
npm test は、ドキュメントスナップショットをデコードする関数 readCache のみをカバーします。2つのコールバック、トランザクション、および createFirestoreTokenCachePlugin には自動テストがありません — それらは Firestore バックエンドを必要とします。それらを実行するにはエミュレータが必要で、PATH に java とそれ自身のインストールが必要です:
sudo apt-get install google-cloud-cli-firestore-emulatorgcloud components install cloud-firestore-emulator は、Debian パッケージの Google Cloud CLI にはインストールされません。そのビルドではコンポーネントマネージャーが無効になっており、gcloud は代わりに上記の apt-get コマンドを出力します。
Graph 認証
src/graph-auth.ts は、シードされたトークンキャッシュを Microsoft Graph アクセストークンに変換します。createGraphAuth は、ONENOTE_CLIENT_ID、ONENOTE_AUTHORITY、および Firestore キャッシュプラグインから1つの PublicClientApplication を構築し、プロセスの存続期間中保持します。getAccessToken() はキャッシュされたアカウントを読み取り、acquireTokenSilent を呼び出し、トークンを返します。要求されるスコープは Notes.Read と Notes.ReadWrite で、完全修飾されています。
デプロイされたサーバーは対話的にサインインすることはありません。誰かにプロンプトを表示する方法がなく、Graph の OneNote エンドポイントはアプリ専用認証をサポートしていないため、保存されたリフレッシュトークンが失効した場合のフォールバックはありません — 人間が npm run bootstrap を再実行します。したがって、すべての障害はそれを示す GraphAuthError であり、呼び出し元に Graph からの素の 401 として届く生の MSAL エラーではありません:
| 何が起こったか | 何をすべきか |
| Firestore ドキュメントが存在しない、またはその |
|
| Firestore が応答しなかった、またはランタイムサービスアカウントが | 再試行。サインインではありません。 |
| キャッシュは読み取られたが、サインインしたアカウントが含まれていない |
|
| 保存されたリフレッシュトークンが期限切れまたは失効している、またはトークンエンドポイントが使用可能なものを返さなかった |
|
cache-unavailable は価値のある行です。Firestore は acquireTokenSilent 内でキャッシュプラグインを通じて読み書きされるため、バックエンドの停止は以前は期限切れのリフレッシュトークンが生成するのと同じ拒否として届きました — そしてそのメッセージはオペレーターにブラウザに行って動作している資格情報を置き換えるように指示します。GraphAuthError.retryable がその区別を伝え、その理由だけがそれを設定します。
これらのそれぞれも stderr に1行書き込みます:
{"event":"graph-auth-failure","reason":"silent-failed","documentPath":"tokencache/msal","retryable":"false"}その行が要点です。ツールの障害はそれ以外は Claude 会話内にのみ現れるため、それがなければコネクタが動作を停止したことをオペレーターに伝えるものはありません。下の アラート を参照してください。
メッセージはドキュメントパスと基になる MSAL エラーを名前で示し、意図的にアカウント識別子を含みません: username はユーザーの UPN であり、homeAccountId はテナント ID を埋め込んでおり、どちらもログに含めるべきではありません。
npm test はフェイククライアントを通じて取得ロジックをカバーします。createGraphAuth 自体には自動テストがありません: 実際のデバイスコードサインインによってシードされたキャッシュが必要であり、それをシードできる資格情報はコミットしてはなりません。npm run bootstrap を実行し、同じドキュメントに対してサーバーを実行してそれを実行します。その消費者は以下の Graph 構造クライアントです。どちらもまだ createApp に配線されていません。
Graph 構造
src/graph-structure.ts は OneNote ツリーを読み取ります: ノートブック、セクショングループ、セクション、および1つのセクション内のページリスト。new GraphStructure(auth) は getAccessToken() を持つ任意のものを受け取るため、サーバーは上記の GraphAuth を渡します。
メソッド | 戻り値 |
| 表示名によるすべてのノートブック |
| ノートブックまたはセクショングループの直下のセクション |
| ノートブックまたはセクショングループの直下のセクショングループ |
| 上記の両方をまとめて取得 |
| 1つのセクション内のページ、最近変更された順、最大 |
| ネストされたすべてのセクショングループが解決された1つのノートブック |
| すべてのノートブック、それぞれのツリーが解決されたもの |
| セクションと1レベルのセクショングループを持つすべてのノートブックを、単一のリクエストで取得 |
| アカウント内の名前がそのテキストを含むセクション、それぞれの親ノートブックとセクショングループ付き |
| タイトルが一致する1つのセクション内のページ、Graph によって大文字小文字を区別せず比較 |
containerKind は notebooks または sectionGroups — 2つの Graph リレーションシップ名です。両方のコンテナ種類が同じ子リレーションシップを公開するため、リストメソッドは2回存在する代わりに種類を受け取ります。
getExpandedTree() は安価なものです。Graph にリレーションシップをウォークする代わりに展開するよう要求します:
GET /me/onenote/notebooks?$select=id,displayName
&$expand=sections($select=id,displayName),
sectionGroups($select=id,displayName;$expand=sections($select=id,displayName))54ノートブックのアカウントに対して測定: 1リクエストと78 KB、getFullTree() の195リクエストに対して。OneNote が1時間に400リクエストと5つの同時実行を許可するため、これは重要です。各展開句内の $select がレスポンスを441 KBから78 KBに減らすものであり、$select と $expand の両方を持つ句内のセパレータはセミコロンです。到達しないのはセクショングループ内にネストされたセクショングループです — Graph は $expand のネストを2レベルに制限します — そのため findSectionsByName は代わりに1リクエストでそのケースをカバーし、アカウント全体のセクションリストをフィルタリングし、各セクションの親を展開します。
api-overview.md は、これらのエンドポイントが受け入れるもの、サービスが独自のドキュメントと矛盾する場所を含めて記録しています。
単一の Graph 呼び出しが処理しない3つのことをトラバーサルが処理します:
ネスト。 セクショングループは UI の「タブグループ」であり、さらにセクショングループを含みます。
getNotebookTreeは再帰します。ページング。 すべてのリスト呼び出しは、
@odata.nextLinkが表示されなくなるまでそれに従います。Graph は独自のページサイズを選択し、より大きな$topを無視するため、1つのレスポンスがコレクションが完全であることの証明にはなりません。listPagesInSectionはtopアイテムが手元にあるとすぐに停止するため、topはページサイズではなく結果数です。アカウント全体のページリストは決して呼び出されません。
GET /me/onenote/pagesは、ノートブックごとの年の構造でエラー 20266「最大セクション数を超えました」で失敗します。ページリストは常に/me/onenote/sections/{id}/pagesにスコープされ、テストはsrc/をアカウント全体のパスについてスキャンします。
失敗は、2xx以外のレスポンスの場合 GraphRequestError です。これは status、statusText、レスポンスの body を保持します。エラー20266は、そのテキストによってのみ他の400と区別できるためです。また、2xxでもボディが期待した形でない場合、終了しないリスト、または20レベルを超えてネストされたセクショングループの場合は GraphResponseError です。どのメッセージにもノートブック、セクション、ページ名は含まれません。
npm test は、正確なURLをキーにしたフェイクの fetch を通してすべてを駆動します。これで確認できないのは、GraphがそれらのURLを受け入れるかどうかです。クエリ文字列は project-spec.md の付録Aにある検証済みの recon スクリプトから来ており、実際のテナントに対して実行することでのみ確認されます。
インク
Graphの通常のページコンテンツエンドポイントは手書きを落とし、<!-- InkNode is not supported --> を残します。また、Graphはページを画像やPDFとしてエクスポートできません。そのため、手書きは生のストロークデータから再構築されます。
GET /me/onenote/pages/{id}/content?includeInkML=true は multipart/mixed で応答し、一方のパートは同じHTML、もう一方はInkMLです。ストロークはSVGになり、次にPNGになり、呼び出し元のモデルに画像として渡され、モデル自身の視覚で読み取られます。OCRサービスは関与しません。
モジュール | 機能 |
|
|
|
|
|
|
これが機能するかどうかを決定する4つの詳細があり、そのすべてが project-spec.md の付録Aにある検証済みの recon スクリプトから来ています。
名前空間は削除されます。 Graphは
inkml:ink、inkml:trace、inkml:traceFormatを出力します。fast-xml-parserはremoveNSPrefix: trueで構成され、すべてのルックアップはベア名を使用します。チャネル順序は
<traceFormat>から来ます。 このアカウントのポイントは X、Y、F で、F はペンの筆圧です。各ポイントの最初の2つの数値を読み取ると、筆圧が座標として描画されます。座標は himetric です。
px = himetric * 96 / 2540。これはページHTMLが入力されたコンテンツを配置するのと同じ座標空間であるため、インクと入力されたコンテンツは後で算術によって互いに位置合わせできる可能性があります。トレースはツリー内のどこにでもあります。 ページには複数の
<ink>ルートを含めることができ、<traceGroup>要素はネストされます。それらはすべて収集されます。
インクのないページは null にレンダリングされます。これは入力されたページの通常の応答であり、エラーではありません。実際に発生する失敗は、50レベルを超えてネストされたトレースグループの InkParseError と、resvgが拒否するドキュメントの InkRenderError です。どちらのメッセージもドキュメントの内容を再現しません。ストローク座標はユーザーの手書きだからです。
test/fixtures/*.inkml は手書きで作成されています。数ストローク、X/Y/Fチャネル順序、himetric単位、2つの <ink> ルートとネストされた <traceGroup> 要素を持つ1つのファイルです。キャプチャされたページダンプをコミットしてはなりません。レンダリングされたインクは完全に読み取れる個人メモだからです。
MCPエンドポイント
サーバーは、ステートレスなStreamable HTTP で POST /mcp においてMCPを話します。すべてのリクエストは独自のMCPサーバーを構築し、応答し、破棄します。次のリクエストまで何も残りません。セッションIDはなく、SSEもありません。GET /mcp と DELETE /mcp は405で応答し、POSTはストリームを開かずにJSONボディで応答します。開いたストリームはCloud Runインスタンスを生かしたままにし、アイドル時間に対して課金されます。
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}Streamable HTTP仕様では、このサーバーがストリーミングしない場合でも、両方の Accept タイプが必要です。src/tools.ts の createTools がレジストリです。6つのブラウジングツール(list_notebooks、list_sections、list_pages、search_pages、find_page_by_name、list_pages_by_name)、1つの読み取りツール(get_page_content)、5つの書き込みツール(append_to_page、append_to_page_by_name、create_page、create_page_by_name、update_page_title)があり、src/mcp-server.ts がそれらの周りのJSON-RPCサーフェスです。
スローするツールは、isError: true と読み取り可能なメッセージを持つツール結果として返されます。期限切れのリフレッシュトークン、消えたページ、resvgが拒否するドキュメントはすべて通常の結果であり、プロトコル障害ではありません。登録されていないツールへの呼び出しだけがJSON-RPCエラーです。
すべてのリクエストは1つのJSONログ行を書き込みます。HTTP動詞、パス、ステータス、期間、JSON-RPCメソッド、および tools/call のツール名です。クエリ文字列、ヘッダー、引数、結果は決して含まれません。src/logging.ts を参照してください。
/mcp はベアラートークンの背後に閉じられています。MCPエンドポイントのベアラートークン を参照してください。ヘルスエンドポイントは開いたままです。
OAuthディスカバリ
Claudeはフローを開始する前に認可サーバーを見つける必要があります。src/oauth-router.ts はSDKの mcpAuthRouter をアプリケーションルートにマウントします。これはマウントポイントではなく発行者URLからパスを構築するため、プレフィックスの背後に置くことはできません。そして5つのルートを提供します。これらはすべて必然的に認証なしです。
パス | 内容 |
| RFC 8414 認可サーバーメタデータ |
| RFC 9728 保護リソースメタデータ |
| 認可エンドポイント |
| 同意フォームが投稿される場所。SDKの |
| トークンエンドポイント |
curl -s localhost:8080/.well-known/oauth-authorization-server
curl -s localhost:8080/.well-known/oauth-protected-resource/mcp両方のドキュメントのすべては MCP_PUBLIC_URL から派生しています。それは発行者であり、resource 識別子はそれに /mcp を加えたものです。MCP_PUBLIC_URL は、末尾にスラッシュが付いている場合、起動時に拒否されます。これにより、パスを連結して構築されるすべてのURLが整形式になります。issuer フィールドはURL正規化された形式を報告します。これはオリジンのみの値の場合、末尾にスラッシュが追加された同じ文字列です。保護リソースドキュメントは、パスが接尾辞されたURLでのみ提供されます。ベアの /.well-known/oauth-protected-resource は404であり、/.well-known/openid-configuration も同様です。Claudeは最初に接尾辞付きパスをプローブします。
scopes_supported は offline_access をリストします。これにより、Claudeはアクセストークンが期限切れになるたびに再同意するのではなく、リフレッシュトークンを要求します。registration_endpoint はありません。クライアントIDとシークレットが構成されているため、動的クライアント登録は何もすることがありません。1つのクライアントが登録されており、3つのリダイレクトURIがあります。ホスト型Claudeサーフェス用の https://claude.ai/api/mcp/auth_callback と、Claude Code用の http://localhost/callback と http://127.0.0.1/callback です。ポートはRFC 8252に従って無視されます。
GET /authorize はリダイレクトではなく同意ページをレンダリングします。付与される内容と認可コードが送信されるホストを指定する1つの承認ボタンがあります。承認すると POST /consent に投稿され、60秒の単回使用コードが作成され、クライアントのコールバックにリダイレクトされます。認可リクエスト全体は、MCP_TOKEN_SIGNING_KEY で署名された1つの隠しフィールドでそのページを通過するため、同意中のインスタンス置換がフローを壊さず、フォームを編集できません。検証に失敗するフィールドは、リダイレクトもコード発行もない400です。
両方の同意レスポンスは Cache-Control: no-store、Referrer-Policy: no-referrer を運びます。フォームは /authorize URLから投稿され、そのクエリ文字列には state とPKCEチャレンジがあります。X-Frame-Options: DENY、およびCSP default-src 'none'; style-src 'unsafe-inline'; frame-ancestors 'none'; base-uri 'none' も同様です。意図的に form-action はありません。ブラウザーはリダイレクトターゲットに対してチェックされるかどうかについて意見が分かれており、同意POSTはclaude.aiへのリダイレクトで応答するためです。
POST /consent には独自のレート制限があります。15分間に200回です。これは、SDKの /authorize リミッターの前に意図的にマウントされており、レンダリングされたフォームは10分間投稿可能なままなので、/authorize を1回通過すると、リプレイ可能なフィールドが生成されるためです。制限は100エントリの保留コードキャップの上にあり、ストア自身の退避(その動作は指定されています)がバーストが最初に遭遇するものになるようにします。
POST /token は、1時間有効なアクセストークンと30日有効なリフレッシュトークンを発行します。どちらも MCP_TOKEN_SIGNING_KEY の下でのコンパクトなペイロードに対するHMAC-SHA256であり、他には何もありません。検証のためにストアは参照されません。これにより、Cloud Runリビジョン置換が再接続を強制しないようになります。ペイロードはオーディエンスを運びます。これは MCP_PUBLIC_URL に /mcp を加えたものであり、トークンはこのMCPエンドポイントにのみ有効で、他には有効ではありません。これらのトークンがどれだけ長く生きるか、そして人間にもっと頻繁に承認させる方法は、2つのセクション下にあります。
MCPエンドポイントのベアラートークン
/mcp へのすべてのリクエストには Authorization: Bearer <アクセストークン> が必要です。SDKの requireBearerAuth は createApp のMCPルーターの前にあり、src/oauth-provider.ts の verifyAccessToken がそれが呼び出すものです。MCP_TOKEN_SIGNING_KEY の下でのHMAC署名、トークンの種類、有効期限、オーディエンスです。正しく署名され、期限切れでないが、別のサーバーのリソース識別子を運ぶトークンは拒否されます。SDKは独自のオーディエンスをチェックしないため、そのチェックがなければ、この署名キーを共有するサーバーによって別のMCPサーバー用に発行されたトークンが受け入れられるでしょう。
トークンがない、期限切れ、またはこれらのチェックのいずれかに失敗するリクエストは、チャレンジヘッダー付きの 401 です。
WWW-Authenticate: Bearer error="invalid_token", error_description="…",
resource_metadata="https://<MCP_PUBLIC_URL>/.well-known/oauth-protected-resource/mcp"resource_metadata パラメータが重要な部分です。これによりClaudeは認可サーバーを見つけてフローを開始するため、それなしの401はサインインプロンプトではなく行き止まりです。Claudeは401でリアクティブにリフレッシュし、保存された有効期限の数分前にプロアクティブにリフレッシュするため、ここでの401は通常のイベントです。
トークンは Authorization ヘッダーからのみ読み取られ、他の場所からは読み取られません。クエリ文字列の ?access_token= は尊重されません。MCP認可仕様がそれを禁止しており、src/logging.ts はその強さでログ行からクエリ文字列を除外します。
どのルートが開いているかは除外リストであり、/mcp 以外のすべてよりも長いです。認可フロー全体がまだトークンを保持していない呼び出し元に応答する必要があるためです。/healthz と /health、両方の .well-known ドキュメント、/authorize、/consent、/token です。test/server.test.ts のテストは、createApp が実際に登録するルートを列挙し、そのリストにないすべてのルートがトークンなしで401で応答することをアサートします。そのため、後で追加されたルートは、誰かが意図的に開かない限り閉じられます。
スコープは必要ありません。このサーバーが発行する唯一のスコープである offline_access は、呼び出し元が何をしてもよいかではなく、リフレッシュトークンが付与されるかどうかに関するものであり、それを要求すると、それ以外は有効なトークンに対して403で応答することになります。スコープチェックが追加された場合、403は WWW-Authenticate: Bearer error="insufficient_scope" を運ぶ必要があります。このミドルウェアはそれを実行します。Claudeは他の403を終端として扱い、何もプロンプトしないためです。
トークンの有効期間と再検証の強制
このサーバーは無人で動作するように作られています。デフォルト設定はその方針を反映しており、漏洩した認証情報を遮断する能力の一部を犠牲にしています。重要な環境にデプロイする前にこれを読み、そのトレードオフが適切でない場合は数値を変更してください。
デフォルト設定の内容
トークン | 有効期間 | 更新するもの |
アクセストークン | 1時間 | リフレッシュトークン(自動) |
リフレッシュトークン | 30日 | 更新のたびに新しい30日間のトークンが発行される |
同意フォーム | 10分 | なし。期限切れのフォームは拒否され、フローが再開する |
Claudeは自動的に更新します。時間切れの前に積極的に、また401を受信した際に反応的に更新します。つまり、コネクタが最初に追加されたときに人間が「承認」をクリックし、その後はコネクタが30日間使用されない場合にのみクリックが必要です。これがスライディングウィンドウです。30日という期間は接続がアイドル状態でいられる時間を制限するものであり、接続の存続期間を制限するものではありません。
スライディング方式の理由とそのコスト
このサーバーが発行するすべてのトークンはステートレスです。署名されたペイロードに過ぎず、データベースの行もセッションレコードも、戻ってきたときに参照するものは何もありません。これこそが、Cloud Runのリビジョン置き換えを透過的にする理由です。新しいインスタンスは、古いインスタンスが発行したトークンを、両者の間で共有状態なしに検証します。トークンストアがあると、デプロイのたびに再接続が必要になります。
そのコストは、個別に失効させることができないことです。失効エンドポイントはありません。削除するものがないからです。具体的には:
漏洩したリフレッシュトークンは最大30日間アクセスを許可し、使用のたびに保持者のアクセスをさらに30日延長します。無効化するためのサーバー側の記録はなく、盗まれたリフレッシュトークンと正規のものを区別する方法もありません。両者は同じ鍵で署名された同じバイト列だからです。
ウィンドウをスライドさせることはローテーションではありません。リフレッシュ時に新しいリフレッシュトークンが発行されても、置き換えられたトークンは内部に刻印された有効期限まで動作し続けます。真のローテーションとは古いトークンを使用済みとマークすることであり、この設計にはないストアが必要です。
アクセストークンも、同じ理由で1時間の途中で遮断することはできません。
残されているのは1つの単純な手段で、即座に効果があります。MCP_TOKEN_SIGNING_KEYを変更して再デプロイすることです。すべてのアクセストークン、すべてのリフレッシュトークン、すべての未完了の同意ページが同時に無効化されます。なぜなら、それらはすべてその鍵に対して検証されるからです。次のClaudeのリクエストは401を受け取り、オペレーターは一度「承認」をクリックします。鍵を定期的にローテーションすることは、それ自体が合理的なポリシーです。
同意画面は、言ってしまえば誰も認証しません。ボタンが1つあるだけで、パスワードはありません。部外者とあなたのノートブックの間に立ちはだかるのは、POST /tokenが要求するMCP_OAUTH_CLIENT_SECRET、すべての認可コードをclaude.aiまたはループバックに送るリダイレクトURI許可リスト、そしてコードをフローを開始したクライアントに結び付けるPKCEです。
人間の承認をより頻繁に求めるには
これらはすべて設定値ではなくソースコードの変更です。これは意図的です。ウィンドウを短縮するオペレーターはデプロイのセキュリティ態勢を変更しているのであり、それは誰かが忘れるかもしれない環境変数ではなく、誰かが読めるコミットに属するものです。
アイドルウィンドウを短縮する。 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その期間使用されないと、コネクタはクリックを必要とします。定期的に使用されていれば、決して要求されません。ウィンドウはスライドし続けるからです。これは、漏洩したリフレッシュトークンが、漏洩が使用されなくなった後に生存する期間を制限するものであり、それ以上のものではありません。
ウィンドウのスライドを止める。 これはissue #22で当初指定されたもので、接続のアイドル時間ではなく総寿命に上限を設けます。コネクタがどれほど忙しくても、人間は30日ごとに承認します。src/oauth-provider.tsのexchangeRefreshToken内の1行:
// 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);リフレッシュトークンの発行を完全に拒否する。 最も厳格な設定です。アクセストークンの有効期限が切れても更新するものがないため、人間は毎時間承認します。2つの編集が必要で、両方とも必須です。メタデータの切り替えだけではトークンの発行は止まりません。
src/oauth-router.tsでSCOPES_SUPPORTEDを空にします。Claudeは、メタデータがoffline_accessを宣伝している場合にのみ、認可リクエストにそれを追加します。これがリフレッシュトークンを要求するかどうかを決定するスイッチです。src/oauth-provider.tsで、issueTokensが返すものからrefresh_tokenフィールドを削除します。現在は要求されたスコープに関係なく発行されています。
これは使用中に目に見える形で現れると予想されます。時間が切れると、Claudeはセッションの途中でブラウザを同意画面に戻します。
アクセストークンを短縮する。 src/oauth-provider.tsのACCESS_TOKEN_TTL_Sは、漏洩したアクセストークンが機能するウィンドウを狭めます。有効期限ごとに1回のトークンリクエストがかかり、人間の関与はまったく不要なので安価です。しかし、漏洩したリフレッシュトークンには何の効果もありません。リフレッシュトークンこそが心配すべき認証情報です。
キープアライブ
Microsoftの委任されたリフレッシュトークンは、約90日間使用されないと失効します。トークンは実際に交換されたときだけスライドし、交換されるのは保持しているアクセストークンの有効期限が切れた後にツール呼び出しが到着したときだけです。つまり、3ヶ月間誰も使用しないコネクタは、ブラウザでnpm run bootstrapを実行する人間が必要なコネクタです。サーバー内の何もそれを単独で防ぐことはできません。誰も呼び出していないときは、サーバー内の何も実行されないからです。
POST /keepaliveがその修正策です。これはforceRefresh: trueを指定してacquireTokenSilentを呼び出します。これにより保持されているアクセストークンをスキップしてリフレッシュトークンを交換するため、Entraは新しいウィンドウを持つ代替トークンを発行し、src/token-cache.tsがそれをFirestoreに書き込みます。forceRefreshが重要な部分です。これがないとMSALは自身のキャッシュから応答し、Entraにリクエストが届かず、ウィンドウは動きません。
MCP_KEEPALIVE_SECRETを少なくとも32文字のランダムな文字列に設定するとルートがマウントされ、未設定のままにするとパスは404を返します。スケジューラーはX-Keepalive-Secretヘッダーでシークレットを提示し、これは何らかの作業が行われる前に定数時間で比較されます。ベアラートークンではなく共有シークレットである理由は、スケジューラーはOAuthフローを実行できないからです。ブラウザがなく、リフレッシュトークンを保持する場所もありません。また、レイヤー1のクライアントシークレットとは別の変数である理由は、MCPサーフェス全体に到達できる認証情報がスケジューラージョブにも置かれないようにするためです。
自分のアカウントでの設定
4つのステップがあります。シークレットを生成し、サービスに設定し、ジョブを作成し、ジョブが実際にサービスに到達することを確認します。これはすべて任意です。スキップしてもサーバーは動作します。90日間誰も呼び出さない日までは。
# 1. Generate the secret. umask 077 so the file is not world-readable, and it is never
# printed: the two commands below read it from disk.
umask 077
openssl rand -hex 32 > keepalive.secret
# 2. Get it onto the service. The GitHub secret alone changes nothing — the value
# reaches the container as an env var, which happens on a deploy and only then.
gh secret set MCP_KEEPALIVE_SECRET < keepalive.secret
gh workflow run deploy.yml --ref main
# 3. Create the job. The API enable takes a minute or two to propagate; a create run
# immediately after it fails with SERVICE_DISABLED, which is a retry rather than a
# misconfiguration.
gcloud services enable cloudscheduler.googleapis.com --project="$GCP_PROJECT"
gcloud scheduler jobs create http onenote-mcp-keepalive \
--project="$GCP_PROJECT" \
--location="$GCP_REGION" \
--schedule="0 4 * * 1" \
--time-zone=UTC \
--uri="$MCP_PUBLIC_URL/keepalive" \
--http-method=POST \
--headers="X-Keepalive-Secret=$(cat keepalive.secret)" \
--attempt-deadline=60s \
--max-retry-attempts=3
rm keepalive.secret--locationは必須で、スケジューラーの独自のリージョンです。これはジョブのターゲットがどこにあるかとは無関係です。Cloud Runのリージョンを使用すれば、覚えておく値が1つ減ります。scripts/gcp-bootstrap.shもcloudscheduler.googleapis.comを有効化するため、ステップ3の有効化は、その行が存在する前にブートストラップされたプロジェクトでのみ必要です。
次に、それを証明します。すべての障害モードが静かだからです。間違ったシークレット、古いURLを指すジョブ、変数なしでデプロイされたサービスは、すべて有効化されたスケジューラージョブのように見えます:
gcloud scheduler jobs run onenote-mcp-keepalive --project="$GCP_PROJECT" --location="$GCP_REGION"
# An empty status code is success. A code is a gRPC status; the request log below says why.
gcloud scheduler jobs describe onenote-mcp-keepalive \
--project="$GCP_PROJECT" --location="$GCP_REGION" \
--format='value(status.code,lastAttemptTime,scheduleTime)'
# The request as the service saw it. 200 is the answer; 401 is a secret mismatch and 404
# means the deploy in step 2 did not happen.
gcloud logging read \
'resource.type="cloud_run_revision" AND httpRequest.requestUrl:"keepalive"' \
--project="$GCP_PROJECT" --limit=3 --freshness=10m \
--format='value(timestamp,httpRequest.requestMethod,httpRequest.status,httpRequest.userAgent)'200はトークンが交換されたことを意味します。保存されたことを証明するのは、FirestoreドキュメントのupdatedAtフィールドが実行時刻に移動することです。これが、置き換えられたリフレッシュトークンが呼び出しを生き延びた唯一の証拠です。このデプロイメントで2026-08-19に測定した結果: 強制実行は200を返し、tokencache/msalは14:15:53Zから14:30:54Zに進みました。
90日のウィンドウに対して毎週で十分であり、数回の実行失敗の余地も残ります。ジョブのコストはトークンエンドポイントへの1往復とFirestoreへの1書き込みです。
シークレットをローテーションするには: 新しいGitHubシークレットを設定し、デプロイし、その後gcloud scheduler jobs update http onenote-mcp-keepalive --update-headers=…を実行します。デプロイとジョブ更新の間の実行は401を返し、何の作業も行いません。毎週のスケジュールでは、その間に何も入り込まないウィンドウです。
ヘッダーはジョブ定義内にあり、プロジェクトに対するroles/cloudscheduler.viewerを持つ誰でも読むことができます。これは、環境変数がCloud Runリビジョン仕様で読めるのとまったく同じです。これはデプロイでSecret Managerを使用しないことと同じトレードオフです。
ステータス | 意味 | スケジューラーがすべきこと |
200 | リフレッシュトークンが交換され、新しいものが保存された | 何もしない |
401 | シークレットがないか間違っている | ジョブを修正する。ルートは何の作業も行わなかった |
404 |
| 設定して再デプロイする |
503( | Firestoreに到達できなかった | 再試行する |
503( | 許可が失効している |
|
これが保護しないもの: 条件付きアクセスのサインイン頻度ポリシー、パスワード変更、MFAリセット、管理者による許可の取り消し。これらのいずれも、スケジュールがどうであれリフレッシュトークンを無効化し、コード変更では回避できません。Entraテナントが自分のものであれば、このアプリ登録をサインイン頻度ポリシーから除外してください。そうでなければ、90日を誰かがあなたに知らせずに短縮できる上限として扱ってください。
キープアライブルートは、以下のトークンの有効期間にあるレイヤー1の30日ウィンドウとも無関係です。そのリフレッシュトークンはClaudeのコネクタストアに存在し、Claudeだけがそれを提示したり、その代替を受け取ったりできます。したがって、ここで実行されているものはそれを生かし続けることはできません。それを失うと、承認ボタンを1回クリックするだけです。Microsoftのものを失うと、デバイスコードによるサインインが必要になります。
ページミラー同期
ミラーは、手動で選んだノートブックセットのページのFirestoreコピーを保持し、読み取りツールがOneNoteのリクエスト予算を消費せずに応答できるようにします。POST /syncがそれを満たす方法です。自動的なものは何もありません。シークレットなしではルートはマウントされず、MIRROR_READ_ENABLEDなしでは読み取りツールはミラーを参照しません。
最新状態を維持するもの
3つのメカニズムがあります。スケジュールで実行されるのは2つだけで、そのうち1つは一時的です。
実行するもの | カバーする範囲 | |
初期バックフィル |
| すべてのミラー対象ノートブック(アクティブかどうかに関わらず)を一度だけ埋める |
継続的キャッチアップ | カバーされる各読み取り(インライン、応答前) | アクティブなノートブックで作成・編集されたページ |
削除スイープ | 毎晩1回のスケジューラージョブ | アクティブなノートブックから削除されたページ |
定常状態ではスケジュールされた増分同期はありません。読み取りがそれを行うからです。 カバーされる各読み取りは、応答する前にrunIncrementalを自身で実行します。最大12リクエスト、最大15秒、30秒に1回までです。以下の読み取り元を参照してください。したがって、ミラーは誰かがそれを見ようとしているときに更新され、それ以外のときは更新されません。その横でスケジュールされた増分実行を行うと、誰も要求していないデータを更新するために同じ1時間あたり400リクエストを消費し、次の読み取りはそれに関係なく再度更新することになります。
読み取りでは削除を検知できない。それこそが夜間スイープが存在する理由そのものである。
増分パスは親セクションの lastModifiedDateTime を鍵としており、作成・編集・削除のいずれもこの値を動かす。そしてそのセクションに対して変更されたページを問い合わせる。削除されたページはどの一覧にも現れないため、その応答には削除されたことを示す情報は一切ない。それを知るには、セクションのページIDを列挙してミラーと突き合わせる必要がある。Graph には OneNote リソースに対する /delta がなく、削除されたページの墓石も存在しないからだ。それはスイープであり、読み取りの前に置くにはあまりに高コストで、スイープが実行されるまでミラーは存在しないページを配信し続ける。一晩の陳腐化をこの設計は許容する。
バックフィルはどちらにも該当しないため、完了するまで独自のスケジュールを持つ。 エポック以降のセクションごとの一覧取得が1回と、ページごとのコンテンツ取得が1回——このアカウントでは数千リクエストになる——であり、インライン更新の12リクエスト予算では数週間かかってしまう。sync/state.backfillComplete が true になった時点でジョブを削除する。これはすべてのミラー対象セクションが非 null の pagesSyncedThrough を持つ時点である。POST /sync を呼び出すものは他にない。
アップグレード後の最初の構造書き込みは、すべてのドキュメントを一度だけ書き直す。 構造ドキュメントはツリーがそのドキュメントについて述べた内容を示す identity 文字列を保持しており、そのフィールドが存在する前に書かれたドキュメントにはそれがない——したがって一致せず、書き込まれる。これは Firestore 書き込みの1パスであり、Graph リクエストはなく、一度だけ発生する。
非アクティブなノートブックは一度バックフィルされ、その後は凍結される。 選択ドキュメント内の activeNotebookIds がその線引きを行う——ミラー対象の選択を参照。アクティブでない選択済みノートブックはバックフィルで埋められ、以後ずっとミラーから配信される。インライン更新もスイープも再び訪れることはない。それこそが55ノートブックのアカウントをミラー可能にする理由である。アーカイブは一度だけ一覧取得・取得され、二度と触れられない。
4つのルート
POST /sync incremental, active notebooks only
POST /sync/sweep scoped sweep, active notebooks only
POST /sync/sweep/full every section of every active notebook
POST /sync/sweep/all every section of every mirrored notebook, active or notこのうち2つはスケジュールに載っている。バックフィル実行中の /sync と、夜間の /sync/sweep/full である。/sync/sweep はより安価なスコープ限定の変種で、手動で使うのに適している。/sync/sweep/all はアクティビティフィルタを持たない唯一のモードであり、非アクティブなノートブック内の削除を検知する唯一の方法である。ミラー対象セクションごとに1リクエストかかるため、タイマーではなく意図的に実行すること。
コスト
OneNote は委任アプリに1時間あたり400リクエストを許可しており、すべての対話型ツール呼び出しも同じ400から消費される。このサービスが対象とするアカウントは55ノートブックと568セクションを保持し、4ノートブックと202セクションがミラー対象、2ノートブックと70セクションがアクティブである。
実行 | リクエスト数 |
インライン更新、変更なし | 1 — 展開されたツリーのみ、それ以外はなし |
インライン更新、編集されたページが数件 | 2〜12。実際に変更されたページのみ取得し、12はハードな予算 |
アクティブなノートブックに対する夜間の | ここでは約70、変更されたページごとに1追加 |
タイムスタンプが動いたセクションに対する | 訪問したセクションごとに1 |
すべてのミラー対象ノートブックに対する | ここでは約202 — 時間予算の半分 |
バックフィルの1回の実行 | 49〜120を測定。先に達した予算で停止する |
変更されていないページはリクエストを消費しない。 変更ページ一覧はすべてのページのスタンプとタイトルを保持しており、両方が一致する保存済みコピーは再取得されない。したがって、実行のコストは直近1時間に編集されたページではなく、実際に変更されたページに追従し、実行レポートの pagesSkipped がそのようにスキップした数を示す。
2つの重複ウィンドウは幅が異なる。これは意図的である。
WATERMARK_OVERLAP_MS は1時間で、セクションの一覧が返すページを決定する。それが表面化させるものはリクエストを消費しないため、余裕を維持する。SECTION_SCAN_OVERLAP_MS は15分で、一度編集されたセクションが訪問され続ける期間を決定し、実行ごとに1回の一覧リクエストがかかる。1時間のそれは削減する価値のある最後の項だった。両方とも Graph の伝播遅延と、Graph の時計とこのサービスの時計の間のギャップに対する余裕である。動作確認の下にある2つのログクエリが、15分が正しいかどうかを示す。
MIRROR_SYNC_REQUEST_BUDGET(デフォルト120)はハードストップである。これに達した実行は、行ったことをコミットし、"done": false で200を返し、次のスケジュールで再開する。それこそが最初のバックフィルを可能にしているものである。
2つの停止条件のどちらが先に効くかは変動するため、観測された平均ではなくリクエスト予算に対して周期を設計すること。 2026-08-20 の6回の実行で測定: 49、57、60、63、120、120リクエスト。最初の4回は240秒の時計で予算に余裕を持って終了した——ページは Graph フェッチ、resvg インクレンダリング、2回の blob 書き込みを要し、レンダリングが1-CPUインスタンスで時計を満たす。最後の2回は、ページのレンダリングがより安価なセクションで、時計内で全予算を使い切った。
周期 | 予算120でのリクエスト/時 | |
| 最大360 | 制限を超えられない最速の周期 |
| 最大480 | 実行が時計制約の間は制限内、そうでないときは超過 |
| 最大720 | 実行がリクエスト制約のときは常に制限超過 |
| 最大1440 | 2026-08-20 にこのアカウントで測定: 45分以内に429秒 |
1時間あたり400を超えると、Graph は OData コード 10007 で429を返す。拒否されたリクエストは作業をせず、それでも試行を消費し、セクションのウォーターマークは進まず、ペナルティは短いバックオフよりも長く続く——3分間にわたる5回の再試行は、1回のバースト後にすべて拒否され、docs/graph.md に記録されている。したがって、制限を超えてもバックフィルは早く完了しない。MIRROR_SYNC_REQUEST_BUDGET を半分にすると、同じ時間あたりの消費で間隔が半分になり、実行ごとにツリー読み取りが1回余分にかかる。
このアカウントでのバックフィルサイズは、おおよそセクションごとに1リクエスト+ページごとに1リクエストである。202セクション×各約24ページ——一覧が完了した最初の4セクションから推定したもので、測定ではない——は約4,800ページと5,000リクエストになり、どの周期でも下限は約12.5時間のスライスとなる。
MIRROR_READ_ENABLED=false の場合、継続的なキャッチアップはまったくない。 これが現在のデプロイ設定であり、バックフィルジョブと夜間スイープだけがミラーに触れる。読み取りを有効にすることが、上記の読み取り駆動設計を現実のものにし、更新ごとに最大12リクエストを30秒に1回以下の頻度で追加する——継続的に忙しい会話から1時間あたり約120リクエスト、ミラーが遅れている間は最大144。バックフィルジョブがなくなれば、その項は夜間スイープとともに400の下に収まる。
セットアップ
# 1. The secret. Never printed; the commands below read it from disk.
umask 077
openssl rand -hex 32 > sync.secret
gh secret set MIRROR_SYNC_SECRET < sync.secret
gh workflow run deploy.yml --ref main
# 2. The backfill job. Temporary — delete it when sync/state.backfillComplete is true.
# --attempt-deadline sits just above Cloud Run's 300s request timeout, so the
# platform's cut is the one that happens and the scheduler records it.
gcloud scheduler jobs create http onenote-mcp-sync \
--project="$GCP_PROJECT" --location="$GCP_REGION" \
--schedule="*/20 * * * *" --time-zone=UTC \
--uri="$MCP_PUBLIC_URL/sync" --http-method=POST \
--headers="X-Sync-Secret=$(cat sync.secret)" \
--attempt-deadline=330s --max-retry-attempts=2
# 3. The nightly deletion sweep. This one is permanent: it is the only thing that
# notices a page deleted out of an active notebook.
gcloud scheduler jobs create http onenote-mcp-sync-sweep-full \
--project="$GCP_PROJECT" --location="$GCP_REGION" \
--schedule="0 0 * * *" --time-zone=America/New_York \
--uri="$MCP_PUBLIC_URL/sync/sweep/full" --http-method=POST \
--headers="X-Sync-Secret=$(cat sync.secret)" \
--attempt-deadline=330s --max-retry-attempts=1
rm sync.secret
# 4. When the backfill finishes.
gcloud scheduler jobs delete onenote-mcp-sync \
--project="$GCP_PROJECT" --location="$GCP_REGION"デプロイ済みアカウントのバックフィルジョブは */15 である。実行が時計制約の間は制限内——2026-08-20 に実行ごと61〜81リクエストと測定——であり、再び120リクエスト予算を使い切るようになると超過する。*/20 はどちらの場合でも超えられない周期である。MIRROR_SYNC_REQUEST_BUDGET を100に下げれば */15 も安全になる。
再試行は両方で安全である。すべてのモードは冪等であり、予算を使い切った実行後の再試行はバックフィル中に積極的に有用である。両方のジョブが存在する間、実行リースにより重複は Graph 予算の二重消費ではなく409になる。
/sync/sweep/all にはジョブがない。activeNotebookIds を編集した後、またはノートブックがこのサーバーの外部で再編成され、ミラーを OneNote が実際に保持しているものと照合する必要がある場合に、手動で実行する。
ミラー対象の選択
選択は手編集される1つの Firestore ドキュメント、MIRROR_ROOT_DOC(デフォルト onenoteMirror/default)である。これはサービスが決して書き込まない唯一のドキュメントである。
フィールド | 型 | 意味 |
| 文字列の配列 | ページがミラーされるノートブックのID |
| 文字列の配列、任意 | そのうちどれがまだ編集されているか。欠落または |
| 文字列 | あなたのための自由文。読み取られることはない。 |
IDは list_notebooks から取得する。構造——アカウント内のすべてのノートブック名とセクション名——は、ツリー読み取りが同じ1リクエストで全部を返し、部分的な構造では list_notebooks とスコープなしの search_pages が自信を持って部分的に答えてしまうため、関係なくミラーされる。ページのコンテンツのみが選択に従う。
activeNotebookIds は継続的な作業を絞り込むだけで、それ以外には影響しない。非アクティブなノートブックのページ、コンテンツ、インクはアクティブなものとまったく同じように保存され、このサーバーを通じた書き込みは依然として到達し、ミラーも更新し、読み取りも依然としてそこから応答する。変わるのは、バックフィルが完了すると増分パスもスイープも再チェックしないことだけであり、OneNote クライアントで非アクティブなノートブックに行われた編集は、ノートブックが再びアクティブになるか /sync/sweep/all が手動で実行されるまでミラーに届かない。
ノートブックを activeNotebookIds に戻すことは安全で、そのノートブックだけのコストで済む。次の実行で保存済みリストを差分し、アクティブになった1つのIDを見つけ、そのノートブックのセクションをそれぞれのセクション別ウォーターマークに対して再一覧する。他のすべてのノートブックはウォーターマークを維持し、一覧されない。実際に変更されたページだけが取得される。notebookIds にノートブックを追加する場合も同じように動作する。
notebookIds 内のどのノートブックにも一致しないIDは、すべての実行レポートで unknownNotebookIds として報告される。activeNotebookIds 内のミラー対象外ノートブックを指すID——タイプミス、または notebookIds に追加されたことのない実在のノートブック——は unknownActiveNotebookIds として報告され、mirror-selection-unknown-active としてログに記録される。編集後は両方を確認すること。IDは不透明な文字列であり、タイプミスはそうでなければ黙って同期されないノートブックになる。
動作確認
# While the backfill job exists. Once it is deleted, the sweep job is the one to force,
# and a covered read is what exercises the incremental path.
gcloud scheduler jobs run onenote-mcp-sync --project="$GCP_PROJECT" --location="$GCP_REGION"
# The report the run returned, in the service log.
gcloud logging read \
'resource.type="cloud_run_revision" AND jsonPayload.event="sync-completed"' \
--project="$GCP_PROJECT" --limit=3 --freshness=30m --format='value(jsonPayload)'200だけでは証明にならない。証明になる3つのこと:
Firestore で
sections/{id}.pagesSyncedThroughが動いたこと。200を返してウォーターマークを進めなかった実行は何もしていない。バケットがインク付きページごとにオブジェクトを保持していること、そしてそのバイト数がページドキュメントの
ink.bytesと一致すること。sectionsSkippedInactiveが期待する数であること。すべてのミラー対象セクションをカバーする数は、アクティブセットが空か誤入力されていることを意味する。同じレポート内のunknownActiveNotebookIdsがどちらかを示す。
2つのログベースの数値が、重複ウィンドウの幅が正しいかどうかを示す。どちらも緊急ではない。サービスが1週間稼働した後に読むこと。
# How far back the page-listing overlap actually reached to catch a page that had really
# changed. The largest ageMs over a day is the narrowest WATERMARK_OVERLAP_MS that would
# still have caught every one of them; approaching 3600000 means that hour is too narrow.
# Tens of minutes is ordinary. Nothing logged at all means the overlap has never caught
# anything.
#
# It says nothing about SECTION_SCAN_OVERLAP_MS. A section that window declines is never
# listed, so it produces no line here at all -- that failure is silence, not a large
# number, and nothing observes it. The nightly /sync/sweep/full is the backstop for it.
gcloud logging read \
'resource.type="cloud_run_revision" AND jsonPayload.event="sync-overlap-save"' \
--project="$GCP_PROJECT" --limit=50 --freshness=1d --format='value(jsonPayload.ageMs)'
# How far this service's clock is from Graph's, in milliseconds, signed. Both overlap
# windows and the settle guard are sized against this being seconds rather than minutes.
# Nothing logged means it stayed under the threshold, which is the expected reading.
gcloud logging read \
'resource.type="cloud_run_revision" AND jsonPayload.event="graph-clock-skew"' \
--project="$GCP_PROJECT" --limit=50 --freshness=1d --format='value(jsonPayload.skewMs)'ジョブを再実行して連続するレポートの done を読むことでバックフィルを監視する: pagesUpdated が上昇する false、次に true。sync/state.backfillComplete も同じことを示し、onenote-mcp-sync ジョブを削除する合図である。
リクエストログで実行中に429を監視してください。バックフィルジョブが1時間あたり400リクエストを超えて実行されている場合、429はその選択の当然の結果であり、対処法は間隔を長くすることです。それ以外の時間に429が発生した場合は、共有リクエストゲートを迂回する何かがあることを意味し、それがアカウント全体がスロットルされる原因となる障害です。
ステータス | 意味 | 対処法 |
200, | 実行が作業を完了した | 何もしない |
200, | リクエストまたは時間の予算で停止した | 何もしない; 次の実行が再開する |
401 | シークレットがないか間違っている | ジョブを修正する; ルートは何も処理しなかった |
404 | サービスに | 設定して再デプロイする |
409 | 別の実行がリースを保持している | 何もしない; 15分後に期限切れになる |
504 | 実行がCloud Runの300秒リクエストタイムアウトを超えた | 何もしない; 下記参照 |
503, | Microsoftの更新トークンが失効している |
|
503, | Firestoreが応答しなかった | 再試行する; 資格情報は問題ない |
読み取りについて
MIRROR_READ_ENABLEDがスイッチです。これがfalse(デフォルト)の場合、すべてのツールはミラーが存在する前とまったく同じようにMicrosoft Graphから応答します。trueの場合、7つのツールがローカルコピーを経由します: list_notebooks、list_sections、list_pages、list_pages_by_name、find_page_by_name、get_page_content、search_pages。
これらの読み取りはそれぞれ3つのステップで構成され、この順序で行われます。
更新。 ツールはそれ自体で増分同期を実行します —
POST /syncが実行するのと同じrunIncrementalで、より小さい予算: OneNoteリクエスト12件と15秒。バックフィルジョブが削除されると、これが増分パスを実行する唯一のものになります。ローカルコピーの読み取り。 ヒットはOneNoteリクエストを消費しません。ミスは以前とまったく同じようにMicrosoft Graphにフォールスルーします。
ソースの報告。
sourceは、回答がOneNoteが保持するものと等しい場合はonenote、回答の一部がオペレーターが凍結したノートブックからのものである場合はbest-available、遅れている可能性がある場合はmirrorです。
sourceルールの完全版。「確認済み」とは、ステップ1の更新が完了したか、回答が単一のページであり、書き込みによって古いとマークされていないことを意味します:
状況 |
|
読み取りがMicrosoft Graphにフォールスルーした |
|
アクティブなノートブックからのローカルヒット、確認済み |
|
アクティブなノートブックからのローカルヒット、未確認 |
|
非アクティブなノートブックからのローカルヒット、確認済みか未確認 |
|
両方をカバーするスコープなしの検索、確認済み |
|
両方をカバーするスコープなしの検索、未確認 |
|
更新が「完了」したとは、ノートブックツリーを読み、必要なすべてのセクションを訪問し、必要なすべてのページを取得し、どちらの予算も使い果たさなかったことを意味します。それ以外のすべて — リクエストまたは時間の予算が尽きた、ツリーの読み取りが500を返した、ページの取得が失敗した、スケジューラーが同期リースを保持していた、Firestoreが応答しなかった — はmirrorであり、回答にはコピーが取得された時刻を示すmirroredAtが含まれます。get_page_contentは最初の3行の例外です: ページドキュメントは独自の古さフラグを持ち、すべての書き込みはOneNoteに触れる前にそのページを古いとマークするため、そこでのヒットは何も置き換えていないページです。
best-availableは劣化した回答ではなく、更新が失敗してもmirrorにはなりません: その更新は非アクティブなノートブックをチェックする予定はなかったため、それを報告することは回答を変更できなかった失敗を説明することになります。それが言うのは、コピーがこのサーバーを通じた書き込みまたは/sync/sweep/allによってのみ再チェックされるということです。list_notebooksはノートブックごとにpagesActiveを報告し、スコープなしのsearch_pagesはnotebooksSearchedの横にinactiveNotebooksを報告するため、モデルはスキップの大きさを確認できます。list_notebooksとlist_sectionsはbest-availableを報告しません — 構造はアクティブなセットが何であれ、すべての実行でアカウント全体に対して保存されます。
更新はすべての呼び出しで実行されるわけではありません。 完了した更新は30秒間保持され、未完了の更新は5分間保持されるため、ツール呼び出しのバーストは30回ではなく1回の更新で済みます。最悪の場合、静かなアカウントで継続的にビジーな会話から1時間あたり約120件の追加のOneNoteリクエストが発生し、制限の400に対して — そして静かなアカウントでの更新は単一のリクエスト、つまりノートブックツリーです。
ライブ読み取りを強制する呼び出しごとの引数はありません。以前はありました(useLiveData); それは廃止されました。なぜなら、最初に更新してから更新が完了したかどうかを示す読み取りは、呼び出し側モデルが推論する必要があるスキーマフィールドなしで同じ質問に答えるからです。
オフにすることは完全なロールバックです: 変数をfalseに設定し、再デプロイすると、ツールはミラー以前の動作とバイト単位で同一になります。更新は実行されず、ツール呼び出しが同期にリクエストを費やすことはなく、すべての回答はsource: "onenote"と言います。データ移行はなく、ミラーはバックグラウンドで埋め続けます。
書き込みはそのページを即座に再同期します。 5つの書き込みツールはすべて、成功した書き込み後にOneNoteからページを再読み取りして保存するため、append_to_pageの直後のget_page_contentは、追加されたばかりのテキストでローカルコピーから回答します。これには書き込みごとにOneNoteリクエストが1件余分にかかります。再同期が失敗した場合、代わりにページが古いとマークされ、次の読み取りがOneNoteに送られます — 遅いですが、決して間違っていません — そして後の読み取りの前の更新がどちらの場合でも修復します。再同期の失敗が書き込みを失敗させることはありません。
オンにする前に確認すべき2つのこと:
4つの複合インデックスすべてが
READYを読み取る。 まだCREATINGのインデックスに対するクエリはFAILED_PRECONDITIONで失敗します。sync/state.backfillCompleteがtrueである。 それより早くオンにすることは安全です — ミラーされていないページはミスとなりGraphに送られます — しかし、ほとんどの読み取りはフォールスルーし、ミラーがない場合よりも遅く、バックフィルが必要とする予算を消費します。
知っておく価値のある1つの動作変更はsearch_pagesです。Graphから回答する場合、セクションを歩き、60セクションまたは25秒で停止するため、sectionsSearched、sectionsFound、stoppedEarlyを報告します。ミラーから回答する場合、歩行も境界もありません — ただし、ページコンテンツは選択したノートブックに対してのみ保持されるため、代わりにnotebooksInAccountに対するnotebooksSearchedを報告し、それらが異なる場合はそのメモでそれを示します。「一致なし」を読むモデルは、どちらを取得したかを知る必要があります。選択範囲外のノートブックを検索するには、sectionIdを渡します: それはローカルコピーでミスし、OneNoteの1つのセクションを歩きます。
実行がオーバーランする場合
MIRROR_SYNC_REQUEST_BUDGETと240秒の壁時計予算はどちらも、Cloud Runの300秒リクエストタイムアウトよりはるかに内側で実行を停止するため、通常の回答は"done": falseの200です。504は、何かが予想より長くかかったことを意味します — ほぼ常に長いRetry-Afterを持つOneNoteからの429です。
そのときに何が起こるかを知っておく価値があります。サービスはCPUスロットリングと最小インスタンス数なしでデプロイされているため、切断されたリクエストは2つの運命のいずれかに直面し、Cloud Runはどちらも保証しません: インスタンスがまだウォームであれば、プロセスは一時停止され、次のリクエストが到着したときに再開されます。ゼロにスケールダウンされた場合、作業は単に失われます。どちらの場合も実行は完了せず、リースは解放されず、再試行はリースが15分後に期限切れになるまで409で応答します。次の実行 — バックフィルジョブ、夜間スイープ、または次の対象読み取りの前の更新 — はその後正常に進行します。
どちらの場合も何も失われません。ウォーターマークはセクションごとに進み、そのセクションのページが保存された後にのみ進むため、切断された実行はコミットしたすべてを保持し、次の実行はそこから再開します。同じことは、実行間で単に消えるインスタンスにも当てはまります: ミラー、Microsoft更新トークン、およびこのサーバーが発行するすべてのトークンは、メモリに保持されるのではなく保存または署名されるため、インスタンスはいつでも消えることができ、トークンの往復1回以上のコストはかかりません。
繰り返し504が発生した場合に確認すべき唯一のことは、リクエストログの429です。持続的なスロットルがタイムアウトに達する唯一の現実的な方法であり、対処法はMIRROR_SYNC_REQUEST_BUDGETを小さくするか、間隔を長くすることであり、Cloud Runのタイムアウトを長くすることではありません。
不良ミラーからの回復
MIRROR_ROOT_DOCのnotebooks、sectionGroups、sections、pages、pageContent、tombstonesサブコレクションを削除し、sync/stateのstructureHashとsectionsScannedThroughをクリアします。次に、セットアップからonenote-mcp-syncジョブを再作成し、backfillCompleteが再びtrueになるまで放置します: 読み取りの前のインライン更新は12リクエストの予算を持ち、ゼロからミラーを再構築しません。選択ドキュメントとトークンキャッシュはこれによって影響を受けず、Graphへの書き込みは関与しません — ミラーはコピーであり、Microsoft Graphが真実のソースのままです。
アラート
2つの障害はログベースのメトリックなしでは見えません。どちらもClaude会話内のメッセージまたは誰も読んでいない行としてのみ表示されるためです:
イベント | 意味 |
| Microsoftの許可が失効しています。誰かが |
| MSALが資格情報のないキャッシュを渡しました。保存されたコピーは生き残りました; 何かが間違っています。 |
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"'次に、そのメトリックがゼロより大きい場合のアラートポリシー。同意の承認も監視する価値があります: POST /consentが302で応答するのは、コネクタを追加するときだけであるべきで、リクエストログはすでにそれを保持しています。
jsonPayload.event="request" jsonPayload.path="/consent" jsonPayload.status=302ブートストラップ
npm run bootstrapは、プロジェクトで唯一の対話型Microsoftサインインであり、Cloud Runではなくあなたのマシンで実行されます。デバイスコードフローでサインインし、結果のMSALキャッシュをサーバーが読み取るFirestoreドキュメントに書き込みます。
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 bootstrapMicrosoftのデバイスコードメッセージを出力し、ブラウザで承認するのを待ってから、ノートブックを一度リストし、トークンが機能する証拠として数を出力します。最後の行は、書き込まれたFirestoreプロジェクトとドキュメント、およびアカウントのホームテナントを指定するため、正しいディレクトリにサインインしたことを確認できます。その出力にはテナントIDが含まれます; それをissue、プルリクエスト、ワークフローログから遠ざけてください。
GOOGLE_CLOUD_PROJECTとFIRESTORE_CACHE_DOCはここでは必須です。サーバーでは前者が推測され、後者がデフォルト値を持つのとは異なります。CLIは独自のアプリケーションデフォルト資格情報で書き込むため、未設定の値はgcloudログインが指すプロジェクトに実際のドキュメントをシードし、それでも成功行を出力します。MCP_OAUTH_*値は読み取られないため、これを実行してもレイヤー1クライアントシークレットがマシンに置かれることはありません。
サーバーのログにretryable: "false"のgraph-auth-failureイベントが表示されるたびに、再度実行してください。更新トークンは使用のたびにローテーションされ、サービスが約90日間アイドル状態になると失効します; 自動回復はありません。上記のキープアライブジョブを設定することが、アイドル状態がそこに到達する方法の1つになるのを防ぐものです。
コンテナ
このサービスはCloud Runにデプロイされ、linux/amd64で動作します。イメージはそのプラットフォーム向けに明示的にビルドされ、@resvg/resvg-jsのネイティブバイナリが一致するようにしています。ランタイムのベースはDebianのnode:24-slimであり、Alpineにしてはなりません。resvgのプリビルドは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は同じものを返し、Cloud Run自身のプローブが使用します。外部から呼び出さないでください。Googleのフロントエンドはhttps://<service>.run.app/healthzに対して独自の404ページで応答し、リクエストはコンテナに到達しないため、外部のアップタイムチェックは/healthを使用する必要があります。2026-08-19にデプロイ済みサービスに対して測定したところ、/health、/healthz2、さらには/Healthzもすべて到達し、正確に小文字の/healthzだけが吞み込まれました。
これらの値は、起動時の検証を通過するのに十分な形式だけを備えたプレースホルダーであり、何に対しても認証しません。Cloud Runでは、PORTはプラットフォームによって提供され、残りはデプロイワークフローから供給されます。ホストのポート8080がすでに使用されている場合は、別のポートにマップしてください: -p 8081:8080と-e PORT=8080を使用します。
イメージをテストするには:
RUN_DOCKER_TESTS=1 bash scripts/test/run.shこれにより、イメージがビルドされ、resvgのglibcバイナリが本番専用インストールで生き残ったこと、開発依存関係が同梱されていないことが確認され、コンテナ内でSVGがPNGにレンダリングされ、PORTで指定されたポートで/healthzが200を返すことが検証されます。RUN_DOCKER_TESTS=1がない場合、dockerスイートはスキップされ、残りは引き続き実行されます。
デプロイ
.github/workflows/deploy.ymlはmainへのプッシュのたびに、またworkflow_dispatchで実行されます。型チェックを行い、npm testを実行し、ビルドし、コンテナイメージをコミットSHAでタグ付けしてArtifact Registryにビルドしてプッシュし、そのイメージをCloud Runにデプロイします。型チェックまたはテストの失敗は、イメージがビルドされる前に実行を停止します。
GitHubには長期有効な認証情報はありません。ジョブはWorkload Identity Federationを通じて認証します: permissions: id-token: writeによりGitHub OIDCトークンを要求でき、google-github-actions/auth@v2がそれを短期間有効なGoogle認証情報と交換します。scripts/gcp-bootstrap.shによってサービスアカウントのJSONキーが作成されることはなく、どこでも必要ありません。プロバイダーはrepositoryクレームがこのリポジトリであるトークンのみを受け入れます。
イメージはubuntu-latestでビルドされます。これはlinux/amd64であり、Cloud Runが実行するプラットフォームであり、@resvg/resvg-jsのプリビルドがコンパイルされているプラットフォームでもあります。そのため、ワークフローはgcloud run deploy --sourceを使用するのではなく、イメージ自体をビルドします。gcloud run deploy --sourceを使用すると、Cloud Buildを有効にして、それに伴うロールを付与することにもなります。
デプロイは--max-instances=1と--allow-unauthenticatedで実行され、ランタイムサービスアカウントとして動作します。このアカウントはFirestoreトークンキャッシュ用のroles/datastore.userを保持しています。--allow-unauthenticatedにより、Claudeがサービスに到達できるようになります。MCPエンドポイントは代わりにベアラートークンで閉じられています。MCPエンドポイントのベアラートークンを参照してください。
リポジトリが保持する必要があるもの
scripts/gcp-bootstrap.shはGCP側をプロビジョニングし、最初の6つに対するgh variable setコマンドを出力します。ワークフローは最初のステップで失敗し、何が欠けているかを名前で示すため、半分設定された状態でデプロイされることはありません。
名前 | 種類 | 値 |
| 変数 | プロジェクトID |
| 変数 | Cloud Runリージョン |
| 変数 | Artifact Registryリージョン |
| 変数 | 完全なワークロードIDプロバイダーリソース名 |
| 変数 | デプロイサービスアカウントのメールアドレス |
| 変数 | ランタイムサービスアカウントのメールアドレス |
| 変数 | Azureアプリ登録のクライアントID |
| 変数 | EntraオーソリティURL |
| 変数 | レイヤー1のOAuthクライアントID |
| 変数 | サービスの公開URL。以下を参照 |
| 変数 | 任意。デフォルトは |
| シークレット | レイヤー1のOAuthクライアントシークレット |
| シークレット | アクセストークンの署名キー。少なくとも32文字 |
| シークレット | 任意。未設定の場合は |
| 変数 | 任意。ページミラーのCloud Storageバケット。 |
| 変数 | 任意。デフォルトは |
| 変数 | 任意。デフォルトは |
| 変数 | 任意。デフォルトは |
| シークレット | 任意。未設定の場合は |
これらのうち、認証情報なのは4つだけです。WIFプロバイダー名とサービスアカウントのメールアドレスは識別子であり、このリポジトリのOIDC IDを提示できない者には役に立たないため、シークレットではなく変数になっています。
デプロイはenv_vars_update_strategy: overwriteを渡すため、ワークフロー内のリストが各リビジョンにおけるサービスの全環境になります。アクションのデフォルトはmergeであり、その場合、ワークフローから削除された変数が前のリビジョンから黙って残ってしまいます。PORTとGOOGLE_CLOUD_PROJECTは意図的にリストに含まれていません。Cloud Runが両方を提供し、PORTを入力として拒否するためです。
最初のデプロイとMCP_PUBLIC_URL
MCP_PUBLIC_URLはOAuth発行者であり、このサーバーが発行するすべてのアクセストークンのオーディエンスです。そして、最初のデプロイが行われるまで、URLを持つサービスは存在しません。ワークフローは3つのステップでこれを解決します: MCP_PUBLIC_URLリポジトリ変数、次にCloud Runがすでにサービスに割り当てたURL、そして—両方が存在しない場合にのみ—https://placeholder.invalidを使用し、デプロイ直後にそれを実際のURLに置き換えます。したがって、最初の実行は無人で機能し、正しい値が配置された状態で完了します。URLを名前で示す警告が残ります。リポジトリ変数にそれを設定してください。なぜなら、サービスの前にカスタムドメインを置いた場合に生き残るのは3つのソースのうちそれだけだからです。
MCP_PUBLIC_URLを変更しても、それ自体では何も無効になりませんが、すでに発行されたすべてのアクセストークンは古いオーディエンスにバインドされており、拒否されます。その場合、Claudeは認可フローを再実行します。
ロールバック
イメージタグはコミットSHAであるため、以前のイメージはまだArtifact Registryにあります:
gcloud run services update-traffic onenote-mcp --region "$GCP_REGION" --to-revisions <revision>=100workflow_dispatchで以前のコミットからワークフローを再実行することも機能し、デプロイされた環境をそのコミットのワークフローファイルと同期させ続ける方法です。
設定
すべての値は環境変数から取得され、起動時に検証されます。欠落または不正な形式の変数は、一度にすべての問題を列挙するConfigErrorを生成し、プロセスはスタックトレースなしで終了コード1で終了します。
変数 | 必須 | デフォルト | 目的 |
| はい | — | Azure アプリ登録のクライアント ID(パブリッククライアント) |
| はい | — | テナントの Entra ID オーソリティ URL |
| はい | — | Claude が提示するレイヤー 1 の OAuth クライアント ID |
| はい | — | レイヤー 1 の OAuth クライアントシークレット |
| はい | — | 発行されたアクセストークンの署名に使用するキー(最小 32 文字) |
| はい | — | サービス自身のパブリック URL: |
| サーバー: いいえ・ブートストラップ: はい |
| MSAL トークンキャッシュを保持する Firestore ドキュメントパス |
| サーバー: いいえ・ブートストラップ: はい | — | GCP プロジェクト。Cloud Run では自動的に推測される |
| いいえ |
| バインドポート。Cloud Run が設定する。サーバーはポートをハードコードしない。 |
| いいえ | — | 最小 32 文字。設定すると |
| いいえ |
| 手編集された |
| いいえ | — | 最小 32 文字。設定すると |
| 条件付き | — | レンダリングされたインクと特大ページ HTML 用の Cloud Storage バケット。 |
| いいえ |
|
|
| いいえ |
| 1 回の同期実行で Graph リクエストが消費できる上限。超過すると停止し、未処理の残りを報告する。10〜350。時間あたり 400 の制限に対して。 |
5 つの MIRROR_* 変数は Firestore ページミラーを設定する。すべてオプションであるため、
これらのいずれも設定せずにデプロイされたサービスは、ミラーが存在する前とまったく同じように動作する。
つまり、MIRROR_READ_ENABLED はコード変更ではなく完全なロールバックスイッチとなる。
MIRROR_BUCKET は loadConfig における唯一のフィールド間ルールである。VarSpec は
変数ごとに必須かどうかを指定でき、「別の変数が存在する場合は必須」とは指定できない。しかし、
同期にはバケットなしでレンダリングされたインク PNG を置く場所がなく、ミラー読み取りには
取得元がなくなる。そのため、MIRROR_SYNC_SECRET を設定するか、MIRROR_READ_ENABLED を
true に設定しても、バケットがない場合は、バックフィルの途中で最初のオブジェクト書き込み時に
失敗するのではなく、コンテナ起動時に MIRROR_BUCKET が欠落リストに含まれた状態で失敗する。
MIRROR_READ_ENABLED は大文字小文字を区別せず true と false のみを受け付ける。
意図的に寛容ではない: 1、yes、on は人間にはすべて true と読めるが、これらを受け入れる
パーサーは off と 0 の意味も決定しなければならず、その時点で ture のようなタイポが
false になり、何の通知もなくミラーがオフになってしまう。
FIRESTORE_CACHE_DOC は src/token-cache.ts の MSAL キャッシュプラグインが読み書きする
ドキュメントを指定する。その値はドキュメントパスでなければならず、つまりスラッシュ区切りの
セグメント数が偶数である必要がある。loadConfig は起動時にコレクションパスを拒否する。
ONENOTE_CLIENT_ID と ONENOTE_AUTHORITY は、src/graph-auth.ts が Entra ID に提示する
Azure アプリ登録を識別する。これはパブリッククライアントであるため、意図的にレイヤー 2 の
クライアントシークレットは存在しない。その下の MCP_OAUTH_* の値はレイヤー 1、つまり
Claude とこのサーバー間のものであり、無関係である。
MCP_PUBLIC_URL は Claude がこのサービスに到達する URL である。OAuth 発行者、トークンが
バインドされる resource 識別子、保護リソースのメタデータドキュメントの URL はすべて
これから構築される。Cloud Run 上では、プロセスがどの URL で到達されているかを知らせるものは
何もなく、Host ヘッダーから取得した値は呼び出し元が送信したものになってしまうため、
これは設定される。これは最初のデプロイで URL が生成された後にのみ埋めることができる。
npm run bootstrap は ONENOTE_CLIENT_ID、ONENOTE_AUTHORITY、
FIRESTORE_CACHE_DOC、GOOGLE_CLOUD_PROJECT のみを読み取る — MCP_OAUTH_* の値は
読まない — そして最後の 2 つはデフォルト化せず必須とする。Bootstrap を参照。
リポジトリの衛生管理
このリポジトリは公開されている。実際のページコンテンツ、レンダリングされたインク、Entra テナント名
または ID、Firestore ドキュメントの内容をコミットしてはならない。.gitignore は output/ と
トークンキャッシュファイルのパターンを除外する。project-spec.md の「Repo hygiene」セクションを参照。
This server cannot be deployed
Maintenance
Related MCP Connectors
Manage Microsoft 365 email, calendar, contacts and inbox rules via the Graph API with OAuth 2.0.
Permissioned access to Outlook, OneDrive and Teams via the user's own Microsoft account
Microsoft OneNote (Microsoft 365) MCP Pack
MCP-native open-source Notion alternative: read & write pages, databases and kanban boards.
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.21 npm27MIT
- 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.21 npm3MIT
- 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.203MIT
- AlicenseNot gradedqualityFmaintenanceEnables natural language access to Microsoft OneNote notebooks, sections, and pages for reading and listing content.53MIT