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_CACHE_DOC から来る単一の Firestore ドキュメントに対して MSAL の ICachePlugin を実装します。beforeCacheAccess はドキュメントの cache フィールドを読み取り、その文字列を MSAL に渡します。afterCacheAccess は、MSAL がキャッシュの変更を報告した場合にのみ、シリアライズされたキャッシュを Firestore トランザクション内で書き戻します。存在しないドキュメントは空のキャッシュとして読み取られます。これは npm run bootstrap が実行される前の状態です。両方のエントリポイントが同じプラグインを使用します: ブートストラップ CLI はそれを通じてキャッシュを書き込み、サーバーはそれを通じて読み取るため、シリアライザは1つで、同期を保つべき別の形式はありません。
そのブロブがリフレッシュトークンの唯一のコピーであるため、2つのことがそれを保護します。
ドキュメントを空にする書き込みは拒否されます。 MSAL は一部の障害時にインメモリキャッシュから資格情報を削除し、afterCacheAccess は MSAL の finally ブロック内で実行されるため、アカウントを失ったシリアライゼーションが、保存されているものがまだ有効な間にこのコードに到達する可能性があります。overwriteWouldEmptyCache がそれを阻止し、{"event":"token-cache-write-refused"} をログに記録します。空チェックは MSAL のキー名を読みません — キャッシュは、すべての値が空のコンテナであるオブジェクトにパースされたときに空になります — そのため、MSAL が形式を変更しても反転せず、認識しないものはブロックされるのではなく通過が許可されます。
各書き込みが置き換えるブロブは previousCache フィールドに保持されます。 履歴ではなく1世代: キャッシュはリフレッシュのたびに書き換えられ、有用なコピーは常に最新の正常なものです。悪い書き込みからの回復は、Firestore コンソールでそのフィールドを cache にコピーすることです。これは、代替手段がデバイスコードサインインであるため、持つ価値があります。ポイントインタイムリカバリを有効にして、第二のレイヤーにしてください:
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 を再実行します。したがって、すべての障害は、Graph からの素の 401 として呼び出し元に届く生の MSAL エラーではなく、その旨を伝える GraphAuthError です:
| 何が起こったか | 対処法 |
| 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「maximum sections exceeded」で失敗します。ページリストは常に/me/onenote/sections/{id}/pagesにスコープされ、テストがsrc/をアカウント全体のパスについてスキャンします。
失敗は、非2xx応答に対しては GraphRequestError です。これは status、statusText、および応答ボディを保持します。エラー20266は他の400と区別できるのはそのテキストによってのみだからです。また、2xx応答であっても、期待された形状でない場合、終了しないリスト、またはセクショングループが50レベルを超えて入れ子になっている場合は GraphResponseError になります。どのメッセージにもノートブック、セクション、ページ名は含まれません。
npm test は、正確なURLでキー付けされた偽の fetch を通じてこれらすべてを駆動します。GraphがそれらのURLを受け入れるかどうかを検証できるのは、そのテストだけではありません。クエリ文字列は project-spec.md の付録Aにある検証済みreconスクリプトから取得され、実際のテナントに対して実行して初めて確認されます。
Ink
Graphの通常のページコンテンツエンドポイントは手書きを削除し、<!-- InkNode is not supported --> を残します。また、GraphはページをPDFにエクスポートできません。したがって、手書きはストロークデータから再構築されます。GET /pages/{id}/content?includeInkML=true はマルチパートレスポンスを返し、ストロークはSVG、次にPNGに変換され、モデルが自分で読めるようになります。
モジュール | 機能 |
|
|
|
|
|
|
|
|
この動作が機能するかどうかは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 = x * 96 / 2540。これはページHTMLがタイプされたコンテンツを配置するのと同じ座標空間であり、後で手書きとタイプされたコンテンツを位置合わせできるようになっています。トレースはページ内のどこにでも現れます。 ページには複数の
<inkml:ink>要素を含めることができ、<inkml:traceGroup>は任意の深さで入れ子にできます。
インクのないページは null を返します(タイプされたページでは正常)。インクの解析に失敗した場合は Error をスローします。失敗したドキュメントの座標は、ユーザーが以前に描いたものなので、エラーメッセージにその座標を含めないでください。test/fixtures/*.inkml には手書きのサンプルがあります。実際のページから取り込んだインクMLをコミットしないでください。レンダリングされたインクは個人情報です。
MCPエンドポイント
サーバーは ステートレスな Streamable HTTP で POST /mcp を話します。各POSTは新しいMCPサーバーインスタンスを作成し、応答後に破棄されるため、セッションは保持されません。GET と DELETE は(この構成では)許可されておらず、Accept ヘッダーは application/json と text/event-stream の両方をリストする必要があります。
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}src/tools.ts はレジストリです – 6つのブラウジングツール(list_notebooks、list_sections、list_pages、get_page_content、find_page_by_name、list_pages_by_name)と1つのライティングツール(append_to_page、create_page、update_page_title)です。src/mcp-server.ts はプロトコルサーフェスです。
ツールの結果は isError: true で返されます。これは、期限切れのトークンや存在しないページなど、アプリケーションレベルの問題に対する通常のMCPの方法です。JSON-RPCエラーは、未知のメソッドに対してのみ発生します。各リクエストは、メソッド名、処理時間、結果のステータスを1行のJSONログに記録します。
/mcp はベアラートークンで保護されています。src/auth.ts を参照してください。
OAuthディスカバリ
Claudeは GET /.well-known/oauth-protected-resource で認可サーバーを発見します。応答には authorization_servers が含まれ、そこから GET /.well-known/oauth-authorization-server が認可エンドポイントとトークンエンドポイントを提供します。
curl -s localhost:8080/.well-known/oauth-authorization-server
curl -s localhost:8080/.well-known/oauth-protected-resource/mcpscopes_supported には offline_access が含まれており、Claudeが認可コードの一部として更新トークンを要求する必要があることを示します。code と refresh_token の両方がサポートされています。
GET /authorize は、承認ボタンと拒否ボタンを備えたHTMLページを提供します。承認すると、認可コードが redirect_uri に渡されます。承認はコードのリストではなく、コードに署名するため、承認エンドポイントが後で変更された場合でも、そのリストは安定しています。
クライアントは MCP_CLIENT_ID と MCP_CLIENT_SECRET で設定されます。これは、クライアントがトークンエンドポイントで認証できるようにするためです。
トークン検証
src/auth.ts の verifyAccessToken は、HMAC-SHA256署名、期限、対象者を検証します。トークンは Authorization ヘッダーからのみ読み取られます。クエリ文字列の ?access_token= は仕様により無視されます。
WWW-Authenticate チャレンジには resource_metadata パラメータが含まれており、Claudeが認可サーバーを発見できるようにします。
トークンにスコープは必要ありません。offline_access はサーバーが発行する唯一のスコープであり、何ができるかではなく、更新トークンが付与されるかどうかを示します。
トークンの有効期間と更新
デフォルトでは、アクセストークンは15分で失効し、更新トークンは30日で失効します。失効した更新トークンは受け入れられません。/token エンドポイントは、更新トークンを新しいアクセストークンと交換する際に、新しい更新トークンも発行します。有効期限が近い場合にのみです。
WWW-Authenticate: Bearer error="invalid_token", error_description="…",
resource_metadata="https://<MCP_PUBLIC_URL>/.well-known/oauth-protected-resource/mcp"トークンにはスコープは必要ありません。offline_access は、保持者のアクセス許可ではなく、更新トークンの付与に関するものです。スコープチェックが追加された場合、403は WWW-Authenticate: Bearer error="insufficient_scope" を返す必要があります。これは、MCP仕様でスコープエラーに必要な唯一のチャレンジ形式だからです。
公開ルートは /healthz、/health、両方の .well-known ドキュメント、/authorize、/consent、/token です。test/server.test.ts のテストは、createApp が登録するすべてのルートを列挙し、そのリストにないルートがトークンなしで401を返すことを確認するため、新しいルートは意図的に開かれない限り閉じられます。
トークンの有効期間と更新
デフォルト
AUTHORIZATION_CODE_TTL_SECONDS– 15分ACCESS_TOKEN_TTL_SECONDS– 15分REFRESH_TOKEN_TTL_SECONDS– 30日更新トークンのローテーション: はい
漏えいしたトークンを無効化する方法
MCP_TOKEN_SIGNING_KEY を変更すると、発行済みのすべてのトークンが即座に無効になります(署名が一致しないため)。これはデプロイのたびにすべてのセッションをログアウトさせることになるため、意図的な侵害対応またはすべてのセッションを強制終了させる方法としてのみ使用してください。
トークンの有効期間
トークン | デフォルト | 環境変数 |
認可コード | 10分 |
|
アクセストークン | 1時間 |
|
更新トークン | 90日 |
|
更新トークンはローテーションされません。/token への refresh_token 付きの各呼び出しは、同じ更新トークンを新しいアクセストークンとともに返します。期限切れの更新トークンは拒否され、新しい認可フローが必要になります。
トークン | 有効期間 | 説明 |
アクセストークン | 1 時間 | リフレッシュトークンにより自動的に更新 |
リフレッシュトークン | 30 日 | 使用のたびに、あらためて30日分の新しいトークンが発行される |
同意フォーム | なし | 期限切れのフォームは拒否され、フローが最初からやり直しになる |
Claude 自体が自発的に、1時間が経過する前にリフレッシュし、また経過後も反応的にリフレッシュします。 つまり、人間が Approve をクリックするのは、コネクタが最初に追加されたときだけで、あとは、コネクタが30日間使われなかったときに限られます。これがスライディングウィンドウです。つまり30日というのは接続がアイドル状態でよい期間の上限であり、接続の寿命そのものではありません。
なぜスライディングなのか、その代償
このサーバーが発行するすべてのトークンは、署名されたペイロードに過ぎず、データベース行もセッション記録も、戻ってきたときに参照するものもありません。それが Cloud Run の新しいリビジョンへの置き換えを透過的にします。新しいインスタンスは古いインスタンスが発行したトークンを検証できるからです。トークンストアがある場合は、デプロイのたびに再接続が必要になります。
その代償として、個別に失効させられるものは何もありません。失効エンドポイントがないのは、削除するものが存在しないからです。具体的には:
漏洩したリフレッシュトークンは最大30日間有効で、使用するたびに保有者のアクセスがさらに30日延長されます。無効化できるサーバー側の記録はなく、正規のリフレッシュトークンと漏洩したものを区別する方法もありません — 両者は同じ鍵で署名された同じバイト列です。
スライディングはローテーションではありません。リフレッシュトークンが新しいリフレッシュトークンを発行するとき、置き換えられた古いトークンは、その埋め込まれた有効期限まで機能し続けます。真のローテーションは古いトークンを使用済みとしてマークすることを意味し、この設計にはそのためのストアがありません。
アクセストークンは、その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);リフレッシュトークンを一切発行しない。 最も厳しい設定です。アクセストークンの期限が切れても、それを更新するものが何もないため、1時間ごとに人間の承認が必要になります。2つの編集が必須で、両方とも必要です。メタデータのスイッチだけではトークンが発行されなくなるわけではないからです。
src/oauth-provider.tsでSCOPESを空にする。Claude は、メタデータがoffline_accessを宣伝している場合にのみ、認可リクエストにそれを追加します。そして、これがリフレッシュトークンを要求するかどうかを決めるスイッチです。src/oauth-provider.tsで、issueRefreshTokenが返すオブジェクトからrefresh_tokenフィールドを削除します。現在はリクエストされたスコープに関係なく発行されます。
これは実際に目に見える形で現れます。セッションの途中でブラウザが同意画面に戻されることになります。
アクセストークンの有効期間を短縮する。 src/oauth-provider.ts の ACCESS_TOKEN_TTL_S は、漏えいした アクセス トークンが機能するウィンドウを狭めます。トークンリクエストが1回増えるだけで、人間の関与は一切不要なのでコストは安価です。ただし、漏えいしたリフレッシュトークンには何の効果もありません。
コネクタを人がもっと頻繁に承認するようにする これらの各設定は、環境変数ではなくソースコードの変更です。間隔を短くする運用者は、デプロイのセキュリティ姿勢を変えていることになります。それは環境変数ではなく、誰かが読めるコミットに残すべきです。
アイドルウィンドウを短くする。 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 内の変更です。
// 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-provider.tsでSCOPES_SUPPORTEDを空にします。Claudeは、メタデータがそれを宣伝している場合にのみoffline_accessを付加します。そしてそれがリフレッシュトークンを要求するかどうかを決めるスイッチです。src/oauth-provider.tsで、issueTokensが返す内容からrefresh_tokenフィールドを削除します。現在は、要求されたスコープに関係なく発行されます。
これは使用中に目に見えるはずです。1時間が経過すると、Claudeはセッションの途中でブラウザを同意画面に戻します。
アクセストークンの有効期間を短縮する。 src/oauth-provider.ts の ACCESS_TOKEN_TTL_S は、漏洩したアクセストークンが有効な時間枠を狭めます。有効期限ごとにトークン要求が1回増えるだけで、人間の関与はまったくありません。しかし、漏洩したリフレッシュトークンには効果がありません。
キープアライブ
Microsoftのリフレッシュトークンは、約90日間使用されないと失効します。サーバー側で何も実行されていない場合、そのトークンを更新できるものは何もありません。POST /keepalive は、その境界を押し戻すためにacquireTokenSilentを呼び出します。forceRefresh: trueが重要な部分です。それがないと、MSALはローカルキャッシュを返すだけで、Entraへのリクエストは発生しません。
MCP_KEEPALIVE_SECRET を少なくとも32文字に設定するとルートが有効になり、設定しないと404になります。X-Keepalive-Secret ヘッダーのシークレットは、何か処理を行う前に一定時間で比較されます。これはベアラートークンではなく共有シークレットです。スケジューラにはブラウザがないため、OAuthフローを実行できず、リフレッシュトークンを保持できないからです。また、これはLayer-1のクライアントシークレットとは別の変数であり、MCPサーフェス全体に到達できる資格情報がスケジューラジョブにも置かれないようにしています。
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=390日のウィンドウに対して週1回で十分であり、数回の実行失敗にも耐えられます。このジョブは、トークンエンドポイントへの1往復とFirestoreへの1回の書き込みしかコストがかかりません。
ステータス | 意味 | スケジューラがすべきこと |
200 | リフレッシュトークンが交換され、新しいものが保存された | 何もしない |
401 | シークレットが存在しない、または誤っている | ジョブを修正する。ルートは何も処理しなかった |
404 |
| 設定して再デプロイする |
503、 | Firestoreが一時的に利用できない | バックオフ付きで再試行する |
503、 | 許可が失効している |
|
これは、条件付きアクセスのサインイン頻度ポリシー、パスワード変更、MFAリセット、または管理者による許可の取り消しからは保護されません。これらはいずれも、スケジュールに関係なくリフレッシュトークンを無効にします。テナントが自分のものである場合は、このアプリの登録をサインイン頻度ポリシーから除外してください。そうでない場合は、90日を、誰かが通知なしに短縮できる上限と見なしてください。
キープアライブルートは、以下のトークンの有効期限にあるLayer-1の30日間ウィンドウとは無関係です。そのリフレッシュトークンはClaudeのコネクタストアに存在し、Claudeだけがそれを提示または交換できます。したがって、ここで実行されているコードはそれを維持できません。失われた場合は、承認ボタンをもう一度クリックするだけです。
アラート
ログベースの指標がなければ、2つの障害は見えません。どちらも、Claudeの会話内のメッセージか、誰も読まないログ行としてのみ現れます。
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サインインであり、あなたのマシン上で実行されます。デバイスコードフローを使用してサインインし、結果の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 bootstrap承認後、スクリプトは一度ノートブックを一覧表示してトークンが機能することを証明し、書き込まれたプロジェクトとドキュメント、およびホームテナントを出力するため、正しいディレクトリにサインインしたことを確認できます。出力にはテナントIDが含まれるため、ログやチケットに貼り付けないでください。
GOOGLE_CLOUD_PROJECT と FIRESTORE_CACHE_DOC は、ここでは必須です(サーバーとは異なり、サーバーでは最初のものは推測され、2番目のものはデフォルト値があります)。CLIは独自のADCで書き込むため、これらの変数がないと、実際には到達できないプロジェクトに本番キャッシュドキュメントを作成し、成功を報告します。MCP_OAUTH_* 変数は読み取られないため、このコマンドを実行してもLayer-1のクライアントシークレットがあなたのマシンに表示されることはありません。
retryable: "false" のgraph-auth-failureイベントが表示されたら再実行してください。Microsoftのリフレッシュトークンは、サーバーが長時間サインインしていなかったために期限切れになっている可能性があります。
デプロイ
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",...}gcloud run deploy は、Cloud RunのUIで「新しいリビジョンとデプロイ」をクリックした場合と同じです。違いは、gcloudが--from-yamlをサポートしている場合でも、YAMLは既存のデプロイを編集するために使用されることです。このワークフローは、Cloud Runサービスの既存のリビジョンを変更するのではなく、新しいリビジョンを作成し、トラフィックを移行します。
RUN_DOCKER_TESTS=1 bash scripts/test/run.sh--verify-... を実行する前に、値が空でないことを確認してください。
サポート
質問はGitHub Discussionsへ。バグはbugラベルを付けてIssueへ。GitHubのメールアドレスがあなたのIDに紐づいていることを確認してください。taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。taintはバグレポートの受け付けを拒否します。
セキュリティレポートはセキュリティアドバイザリを使用してください。taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。
taintを避けてください。
taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。
taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。
taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。
taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。
taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。
taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。
taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。
taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。
taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。
taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。
taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。taintはバグレポートの受け付けを拒否します。
セキュリティ問題は、GitHubのプライベートな脆弱性報告機能を使用して報告してください。taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。taintはバグレポートの受け付けを拒否します。
セキュリティ問題は、GitHubのプライベートな脆弱性報告機能を使用して報告してください。
taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。
taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。
taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。
taintを避けてください。
taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。
taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。
taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。
taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。
taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。
taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。
taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。
taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。taintはバグレポートの受け付けを拒否します。taintを避けてください。taintはバグレポートの受け付けを拒否します。
taintを避けてください。
.github/workflows/deploy.yml は main へのプッシュのたびと workflow_dispatch で実行されます。
型チェック、npm test の実行、ビルド、コンテナイメージのビルドと Artifact Registry へのプッシュ(コミット SHA でタグ付け)、そしてそのイメージの Cloud Run へのデプロイを行います。型チェックまたはテストの失敗は、イメージがビルドされる前に実行を停止します。
GitHub には長期有効な認証情報はありません。ジョブは Workload Identity Federation を通じて認証します。permissions: id-token: write により GitHub OIDC トークンを要求でき、google-github-actions/auth@v2 がそれを短期間有効な Google 認証情報と交換します。サービスアカウントの JSON キーは scripts/gcp-bootstrap.sh によって作成されることも、どこかで必要とされることもありません。プロバイダは 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 コマンドを出力します。ワークフローは最初のステップで失敗し、何が欠けているかを示します。半分だけ設定された状態でデプロイすることはありません。
名前 | 種類 | 値 |
| variable | プロジェクト ID |
| variable | Cloud Run リージョン |
| variable | Artifact Registry リージョン |
| variable | 完全なワークロード ID プロバイダリソース名 |
| variable | デプロイサービスアカウントのメールアドレス |
| variable | ランタイムサービスアカウントのメールアドレス |
| variable | Azure アプリ登録のクライアント ID |
| variable | Entra オーソリティ URL |
| variable | レイヤー 1 の OAuth クライアント ID |
| variable | サービスの公開 URL。以下を参照 |
| variable | 任意。デフォルトは |
| secret | レイヤー 1 の OAuth クライアントシークレット |
| secret | アクセストークン署名キー。32 文字以上 |
| secret | 任意。未設定の場合は |
これらのうち認証情報は 3 つだけです。WIF プロバイダ名とサービスアカウントのメールアドレスは識別子であり、このリポジトリの OIDC アイデンティティを提示できない者には無価値なので、シークレットではなく変数です。
デプロイは 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>=100以前のコミットから workflow_dispatch でワークフローを再実行することも機能し、デプロイされた環境をそのコミットのワークフローファイルと同期させる方法です。
設定
すべての値は環境変数から取得され、起動時に検証されます。欠落または不正な形式の変数は、問題のあるものをすべて一度に列挙する ConfigError を生成し、プロセスはスタックトレースなしで終了コード 1 で終了します。
変数 | 必須 | デフォルト | 目的 |
| はい | — | Azure アプリ登録のクライアント ID(パブリッククライアント) |
| はい | — | テナントの Entra ID オーソリティ URL |
| はい | — | Claude が提示するレイヤー 1 の OAuth クライアント ID |
| はい | — | レイヤー 1 の OAuth クライアントシークレット |
| はい | — | 発行されたアクセストークンの署名に使用されるキー(最小 32 文字) |
| はい | — | サービス自身の公開 URL。 |
| サーバー: いいえ · ブートストラップ: はい |
| MSAL トークンキャッシュを保持する Firestore ドキュメントパス |
| サーバー: いいえ · ブートストラップ: はい | — | GCP プロジェクト。Cloud Run では自動的に推測される |
| いいえ |
| バインドポート。Cloud Run がこれを設定する。サーバーはハードコードしない。 |
| いいえ | — | 32 文字以上。設定すると |
FIRESTORE_CACHE_DOC は、src/token-cache.ts の MSAL キャッシュプラグインが読み書きするドキュメントを指定します。その値はドキュメントパスである必要があり、つまりスラッシュ区切りのセグメントが偶数個である必要があります。loadConfig は起動時にコレクションパスを拒否します。
ONENOTE_CLIENT_ID と ONENOTE_AUTHORITY は、src/graph-auth.ts が Entra ID に提示する Azure アプリ登録を識別します。これはパブリッククライアントであるため、レイヤー 2 のクライアントシークレットは意図的にありません。その下にある MCP_OAUTH_* の値は Claude とこのサーバー間のレイヤー 1 に属し、無関係です。
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 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