Skip to main content
Glama

onenote-mcp

Microsoft OneNote를 Microsoft Graph를 통해 노출하는 MCP 서버 — 노트북 및 섹션 구조, 페이지 콘텐츠, 그리고 호출 모델이 읽을 수 있도록 이미지로 렌더링된 필기 내용을 제공합니다.

project-spec.md가 권위 있는 설계 문서입니다. 잉크 재구성 파이프라인, 두 개의 독립적인 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

스크립트

스크립트

설명

npm run build

src/dist/로 컴파일

npm run typecheck

내보내지 않고 타입 검사

npm run dev

--watch로 소스에서 서버 실행

npm start

dist/에서 컴파일된 서버 실행 (build 먼저 실행)

npm run bootstrap

Firestore 토큰 캐시를 시드하는 로컬 기기 코드 로그인

npm test

test/**/*.test.ts에 대해 node --test 실행

테스트는 test/에 있으며 src/를 미러링합니다. Node의 네이티브 타입 스트리핑을 사용하여 TypeScript 소스에 대해 직접 실행되므로 npm test는 빌드가 필요 없습니다. 이로 인해 소스에 제약이 있습니다: enum, namespace, 생성자 매개변수 속성은 사용할 수 없으며, 타입 전용 가져오기는 import type으로 작성해야 합니다. erasableSyntaxOnlyverbatimModuleSyntax 컴파일러 옵션이 이를 강제합니다.

디렉터리 구조 및 관련 규칙은 CLAUDE.md를 참조하십시오.

토큰 캐시

src/token-cache.ts는 단일 Firestore 문서에 대해 MSAL의 ICachePlugin을 구현하며, 해당 문서의 경로는 FIRESTORE_CACHE_DOC에서 가져옵니다. beforeCacheAccess는 문서의 cache 필드를 읽어 문자열을 MSAL에 전달합니다. afterCacheAccess는 MSAL이 캐시가 변경되었다고 보고할 때만 직렬화된 캐시를 Firestore 트랜잭션 내에서 다시 씁니다. 존재하지 않는 문서는 빈 캐시로 읽히며, 이는 npm run bootstrap이 실행되기 전의 상태입니다. 두 진입점 모두 이 동일한 플러그인을 사용합니다. 부트스트랩 CLI는 이를 통해 캐시를 쓰고 서버는 이를 통해 읽으므로, 하나의 직렬 변환기만 있고 보조 형식은 없습니다.

해당 blob이 새로 고침 토큰의 유일한 사본이므로 두 가지가 이를 보호합니다.

문서를 비우는 쓰기는 거부됩니다. MSAL은 일부 오류 시 메모리 내 캐시에서 자격 증명을 제거하며, afterCacheAccess는 MSAL의 finally 블록 내에서 실행되므로 계정을 잃은 직렬화가 저장된 계정이 아직 유효한 동안 이 코드에 도달할 수 있습니다. overwriteWouldEmptyCache가 이를 중지하고 {"event":"token-cache-write-refused"}를 기록합니다. 비어 있음 검사는 MSAL 키 이름을 읽지 않습니다. 캐시가 모든 값이 빈 컨테이너인 객체로 구문 분석될 때 비어 있기 때문입니다. 따라서 MSAL이 형식을 변경해도 반전될 수 없으며, 인식하지 못하는 것은 차단하는 대신 통과시킵니다.

각 쓰기가 대체하는 blob은 previousCache 필드에 보관됩니다. 기록이 아닌 한 세대: 캐시는 새로 고칠 때마다 다시 작성되며 유용한 복사본은 항상 가장 최근의 양호한 것입니다. 잘못된 쓰기에서 복구하는 것은 Firestore 콘솔에서 해당 필드를 cache에 복사하는 것이며, 대안이 기기 코드 로그인이기 때문에 그만한 가치가 있습니다. 두 번째 계층으로 지정 시간 복구를 켜십시오:

gcloud firestore databases update --enable-pitr

백엔드 오류는 자격 증명 오류가 아닙니다. Firestore에 연결할 수 없거나 roles/datastore.user 바인딩이 해지되면 만료된 새로 고침 토큰이 생성하는 오류로 표시되는 대신 TokenCacheUnavailableError가 발생합니다. 쓰기는 그 전에 세 번 재시도됩니다. 이러한 구분이 왜 그만한 가치가 있는지 아래 표의 cache-unavailable 행을 참조하십시오.

npm test는 문서 스냅샷을 디코딩하는 함수인 readCache만 다룹니다. 두 콜백, 트랜잭션 및 createFirestoreTokenCachePlugin에는 자동화된 테스트가 없습니다. Firestore 백엔드가 필요하기 때문입니다. 이를 실행하려면 PATHjava가 필요하고 자체 설치가 필요한 에뮬레이터가 필요합니다:

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

gcloud components install cloud-firestore-emulator는 Debian 패키지 Google Cloud CLI에 설치하지 않습니다. 해당 빌드에서는 구성 요소 관리자가 비활성화되어 있으며 gcloud는 대신 위의 apt-get 명령을 출력합니다.

Graph 인증

src/graph-auth.ts는 시드된 토큰 캐시를 Microsoft Graph 액세스 토큰으로 변환합니다. createGraphAuthONENOTE_CLIENT_ID, ONENOTE_AUTHORITY 및 Firestore 캐시 플러그인에서 하나의 PublicClientApplication을 빌드하고 프로세스 수명 동안 보관합니다. getAccessToken()은 캐시된 계정을 읽고 acquireTokenSilent를 호출한 다음 토큰을 반환합니다. 요청된 범위는 정규화된 Notes.ReadNotes.ReadWrite입니다.

배포된 서버는 대화식으로 로그인하지 않습니다. 사용자에게 메시지를 표시할 방법이 없고 Graph의 OneNote 엔드포인트는 앱 전용 인증을 지원하지 않으므로 저장된 새로 고침 토큰이 만료되면 대체 수단이 없습니다. 사람이 npm run bootstrap을 다시 실행해야 합니다. 따라서 모든 오류는 Graph의 원시 401로 호출자에게 도달하는 원시 MSAL 오류 대신 GraphAuthError가 됩니다.

reason

상황

수행할 작업

cache-unreadable

Firestore 문서가 없거나 해당 cache 필드가 MSAL이 역직렬화할 수 없는 경우

npm run bootstrap

cache-unavailable

Firestore가 응답하지 않거나 런타임 서비스 계정이 roles/datastore.user를 잃은 경우

재시도. 로그인 아님.

no-account

캐시는 읽었지만 로그인된 계정이 없는 경우

npm run bootstrap

silent-failed

저장된 새로 고침 토큰이 만료되었거나 해지되었거나 토큰 엔드포인트가 유용한 것을 반환하지 않음

npm run bootstrap

cache-unavailable은 그 가치를 인정받는 행입니다. Firestore는 캐시 플러그인을 통해 acquireTokenSilent 내에서 읽고 쓰여지므로 백엔드 중단은 만료된 새로 고침 토큰이 생성하는 것과 동일한 거부로 도착하곤 했습니다. 그리고 그 메시지는 운영자에게 브라우저로 이동하여 작동 중인 자격 증명을 교체하라고 지시합니다. GraphAuthError.retryable은 그 구분을 전달하며 해당 이유만 설정합니다.

이러한 각 항목은 또한 stderr에 한 줄을 씁니다:

{"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 트리(노트북, 섹션 그룹, 섹션 및 한 섹션 내의 페이지 목록)를 읽습니다. new GraphStructure(auth)getAccessToken()이 있는 모든 것을 사용하므로 서버는 위의 GraphAuth를 전달합니다.

메서드

반환

listNotebooks()

표시 이름별 모든 노트북

listSections(containerKind, containerId)

노트북 또는 섹션 그룹 바로 아래의 섹션

listSectionGroups(containerKind, containerId)

노트북 또는 섹션 그룹 바로 아래의 섹션 그룹

listContainerChildren(containerKind, containerId)

위의 두 항목을 함께 가져옴

listPagesInSection(sectionId, top?)

한 섹션의 페이지를 최근 수정된 순서대로 최대 top(기본값 50)개까지

getNotebookTree(notebook)

중첩된 모든 섹션 그룹이 확인된 하나의 노트북

getFullTree()

각각의 트리가 확인된 모든 노트북

getExpandedTree()

단일 요청으로 섹션과 한 수준의 섹션 그룹이 있는 모든 노트북

findSectionsByName(displayName)

계정 전체에서 이름에 해당 텍스트가 포함된 섹션 (각각 상위 노트북 및 섹션 그룹 포함)

findPagesByTitle(sectionId, title)

Graph가 대소문자를 구분하지 않고 비교하여 제목이 일치하는 한 섹션의 페이지

containerKindnotebooks 또는 sectionGroups입니다. 두 Graph 관계 이름입니다. 두 컨테이너 종류 모두 동일한 하위 관계를 노출하므로 목록 메서드가 두 번 존재하는 대신 종류를 사용하는 이유입니다.

getExpandedTree()는 저렴한 방법입니다. Graph에 관계를 단계별로 탐색하는 대신 확장하도록 요청합니다:

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

54개 노트북 계정을 기준으로 측정: getFullTree()의 195개 요청에 비해 1개 요청 및 78KB. OneNote가 시간당 400개 요청과 동시 5개를 허용하므로 중요합니다. 각 확장 절 내부의 $select는 응답을 441KB에서 78KB로 줄이며 $select$expand를 모두 포함하는 절 내부의 구분 기호는 세미콜론입니다. 도달하지 못하는 것은 섹션 그룹 내부에 중첩된 섹션 그룹입니다. Graph는 $expand 중첩을 2단계로 제한하므로 findSectionsByName은 계정 전체 섹션 목록을 필터링하고 각 섹션의 부모를 확장하여 한 번의 요청으로 해당 사례를 대신 처리합니다.

api-overview.md는 이러한 엔드포인트가 허용하는 항목을 기록하며, 서비스가 자체 문서와 모순되는 부분도 포함합니다.

단일 Graph 호출로는 처리하지 못하는 순회가 처리하는 세 가지:

  • 중첩. 섹션 그룹은 UI의 "탭 그룹"이며 추가 섹션 그룹을 포함합니다. getNotebookTree는 재귀합니다.

  • 페이징. 모든 목록 호출은 @odata.nextLink가 더 이상 나타나지 않을 때까지 따릅니다. Graph는 자체 페이지 크기를 선택하고 더 큰 $top은 무시하므로 단일 응답이 컬렉션이 완전하다는 증거는 아닙니다. listPagesInSectiontop 항목이 확보되는 즉시 중지되므로 top은 페이지 크기가 아닌 결과 수입니다.

  • 계정 전체 페이지 목록은 호출되지 않습니다. GET /me/onenote/pages는 연도별 노트북 구조에서 오류 20266 "최대 섹션 수 초과"로 실패합니다. 페이지 목록은 항상 /me/onenote/sections/{id}/pages로 범위가 지정되며 테스트는 계정 전체 경로에 대해 src/를 검사합니다.

실패는 2xx가 아닌 응답의 경우 GraphRequestError입니다. 이 오류는 status, statusText, 응답 body를 담고 있는데, 오류 20266은 다른 400과 구분할 수 있는 유일한 방법이 그 텍스트이기 때문입니다. 그리고 2xx이지만 body가 예상된 형태가 아닌 경우, 종료되지 않는 목록, 또는 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=truemultipart/mixed로 응답하며, 한 부분은 동일한 HTML이고 다른 부분은 InkML입니다. 스트로크는 SVG가 되고 그다음 PNG가 되어, 호출 모델에 이미지로 전달되어 모델 자체의 시각 능력으로 읽습니다. OCR 서비스는 관여하지 않습니다.

모듈

하는 일

src/multipart.ts

splitMultipart(body, contentType) → 파트들, 응답이 multipart가 아닌 경우 null

src/ink.ts

parseInkStrokes(text) → 스트로크; strokesToSvg; rasterizeSvg; renderInk(text, width?) → PNG 또는 null

src/page-content.ts

GraphPageContent.fetchRaw(pageId) → 분할된 응답; .fetchInk(pageId) → PNG 또는 null

이것이 전혀 작동하는지를 결정하는 네 가지 세부 사항이 있으며, 네 가지 모두 project-spec.md의 부록 A에 있는 검증된 recon 스크립트에서 비롯됩니다:

  • 네임스페이스는 제거됩니다. Graph는 inkml:ink, inkml:trace, inkml:traceFormat을 내보냅니다. fast-xml-parserremoveNSPrefix: true로 구성되며 모든 조회는 접두사 없는 이름을 사용합니다.

  • 채널 순서는 <traceFormat>에서 옵니다. 이 계정의 포인트는 X, Y, F이며, F는 펜 압력입니다. 각 포인트의 처음 두 숫자를 읽으면 압력이 좌표로 그려집니다.

  • 좌표는 himetric 단위입니다. px = himetric * 96 / 2540. 이는 페이지 HTML이 입력된 콘텐츠를 배치하는 것과 동일한 좌표 공간이므로, 잉크와 입력된 콘텐츠는 나중에 산술적으로 서로 정합시킬 수 있습니다.

  • 트레이스는 트리의 어디에나 있습니다. 페이지는 둘 이상의 <ink> 루트를 가질 수 있으며 <traceGroup> 요소는 중첩됩니다. 모두 수집됩니다.

잉크가 없는 페이지는 null로 렌더링됩니다. 이는 입력된 페이지의 정상적인 응답이지 오류가 아닙니다. 실제로 발생하는 실패는 50단계를 넘게 중첩된 트레이스 그룹의 InkParseError와 resvg가 거부하는 문서의 InkRenderError입니다. 두 메시지 모두 문서의 어떤 부분도 재현하지 않습니다. 스트로크 좌표는 사용자의 필기이기 때문입니다.

test/fixtures/*.inkml은 직접 작성된 것입니다 — 몇 개의 스트로크, X/Y/F 채널 순서, himetric 단위, 두 개의 <ink> 루트와 중첩된 <traceGroup> 요소가 있는 파일 하나. 캡처된 페이지 덤프는 커밋될 수 없습니다. 렌더링된 잉크는 완전히 읽을 수 있는 개인 메모이기 때문입니다.

MCP 엔드포인트

서버는 무상태 Streamable HTTPPOST /mcp에서 MCP를 말합니다. 모든 요청은 자체 MCP 서버를 구축하고 응답한 다음 해체합니다. 다음 요청까지 남는 것은 없습니다. 세션 ID가 없고 SSE도 없습니다 — GET /mcpDELETE /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.tscreateTools는 레지스트리입니다 — 여섯 개의 탐색 도구(list_notebooks, list_sections, list_pages, search_pages, find_page_by_name, list_pages_by_name), 하나의 읽기 도구(get_page_content), 세 개의 쓰기 도구(append_to_page, create_page, update_page_title) — 그리고 src/mcp-server.ts는 그 주변의 JSON-RPC 표면입니다.

예외를 던지는 도구는 isError: true와 읽을 수 있는 메시지가 있는 도구 결과로 돌아옵니다 — 만료된 리프레시 토큰, 사라진 페이지, resvg가 거부하는 문서는 모두 프로토콜 오류가 아닌 정상적인 결과입니다. 등록된 적이 없는 도구에 대한 호출만 JSON-RPC 오류입니다.

모든 요청은 한 줄의 JSON 로그를 작성합니다: HTTP 동사, 경로, 상태, 지속 시간, JSON-RPC 메서드, 그리고 tools/call의 도구 이름. 쿼리 문자열, 헤더, 인수, 결과는 절대 기록하지 않습니다 — src/logging.ts 참조.

/mcp는 베어러 토큰 뒤에 닫혀 있습니다 — MCP 엔드포인트의 베어러 토큰 참조. 헬스 엔드포인트는 열려 있습니다.

OAuth 검색

Claude는 흐름을 시작하기 전에 인증 서버를 찾아야 합니다. src/oauth-router.ts는 SDK의 mcpAuthRouter를 애플리케이션 루트에 마운트합니다 — 발급자 URL에서 경로를 구성하므로 접두사 뒤에 둘 수 없습니다 — 그리고 다섯 개의 라우트를 제공하며, 모두 필연적으로 인증되지 않습니다:

경로

무엇인가

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

RFC 8414 인증 서버 메타데이터

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

RFC 9728 보호 리소스 메타데이터

GET,POST /authorize

인증 엔드포인트

POST /consent

동의 양식이 게시되는 곳; SDK의 /authorize 라우터는 재개 경로를 소유하지 않음

POST /token

토큰 엔드포인트

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 정규화된 형태를 보고하며, origin 전용 값의 경우 동일한 문자열에 후행 슬래시가 추가된 것입니다. 보호 리소스 문서는 경로 접미사가 붙은 URL에서만 제공됩니다 — 접미사 없는 /.well-known/oauth-protected-resource는 404이고, /.well-known/openid-configuration도 404입니다. Claude는 접미사가 붙은 경로를 먼저 탐색합니다.

scopes_supportedoffline_access를 나열하며, 이는 액세스 토큰이 만료될 때마다 재동의하는 대신 리프레시 토큰을 요청하도록 Claude가 하게 만드는 것입니다. registration_endpoint는 없습니다: 클라이언트 ID와 시크릿이 구성되어 있으므로 동적 클라이언트 등록은 할 일이 없습니다. 하나의 클라이언트가 등록되어 있으며 세 개의 리다이렉트 URI가 있습니다 — 호스팅된 Claude 표면용 https://claude.ai/api/mcp/auth_callback, 그리고 Claude Code용 http://localhost/callbackhttp://127.0.0.1/callback이며, 포트는 RFC 8252에 따라 무시됩니다.

GET /authorize는 리다이렉트 대신 동의 페이지를 렌더링합니다: 부여되는 내용과 인증 코드가 전송될 호스트를 명명하는 하나의 승인 버튼. 승인하면 POST /consent에 게시되며, 60초 단일 사용 코드를 발행하고 클라이언트의 콜백으로 리다이렉트합니다. 전체 인증 요청은 MCP_TOKEN_SIGNING_KEY로 서명된 하나의 숨겨진 필드로 그 페이지를 통과하므로, 동의 중간에 인스턴스가 교체되어도 흐름이 깨지지 않고 양식을 편집할 수 없습니다. 검증에 실패하는 필드는 리다이렉트도 코드 발행도 없는 400입니다.

두 동의 응답 모두 Cache-Control: no-store, Referrer-Policy: no-referrer를 담고 있습니다 — 양식은 쿼리 문자열에 state와 PKCE 챌린지가 있는 /authorize URL에서 게시됩니다 — X-Frame-Options: DENY, 그리고 default-src 'none'; style-src 'unsafe-inline'; frame-ancestors 'none'; base-uri 'none'의 CSP. 의도적으로 form-action이 없습니다: 브라우저들이 리다이렉트 대상에 대해 검사되는지에 대해 의견이 갈렸고, 동의 POST는 claude.ai로의 리다이렉트로 응답하기 때문입니다.

POST /consent는 자체 속도 제한이 있습니다 — 15분에 200회 — SDK의 /authorize 제한기보다 앞에 의도적으로 마운트되어 있고 렌더링된 양식이 10분 동안 게시 가능한 상태로 유지되므로, /authorize를 한 번 통과하면 재생할 수 있는 필드가 생성되기 때문입니다. 제한은 100개 항목의 보류 코드 상한 위에 있어서, 동작이 명시된 저장소 자체의 축출이 폭주가 먼저 부딪히는 대상이 됩니다.

POST /token은 1시간 동안 유효한 액세스 토큰과 30일 동안 유효한 리프레시 토큰을 발행합니다. 둘 다 MCP_TOKEN_SIGNING_KEY 아래의 컴팩트 페이로드에 대한 HMAC-SHA256이며 다른 것은 없습니다 — 검증을 위해 저장소를 조회하지 않으며, 이것이 Cloud Run 개정 교체가 재연결을 강요하지 않게 하는 이유입니다. 페이로드는 대상을 담고 있으며, 이는 MCP_PUBLIC_URL/mcp를 더한 것이므로 토큰은 이 MCP 엔드포인트에만 유효하고 다른 곳에는 유효하지 않습니다. 그 토큰이 얼마나 오래 사는지, 그리고 인간이 더 자주 승인하도록 만드는 방법은 두 섹션 아래에 있습니다.

MCP 엔드포인트의 베어러 토큰

/mcp에 대한 모든 요청은 Authorization: Bearer <access token>이 필요합니다. SDK의 requireBearerAuthcreateApp에서 MCP 라우터 앞에 있으며, src/oauth-provider.tsverifyAccessToken이 호출하는 것입니다: 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는 스스로 갱신합니다. — 1시간이 끝나기 전에 선제적으로, 그리고 401을 받으면 반응적으로 갱신합니다. 그래서 사람은 커넥터를 처음 추가할 때 승인을 클릭하고, 그 후에는 커넥터를 30일 동안 사용하지 않을 때만 클릭합니다. 이것이 슬라이딩 윈도우입니다. 30일은 연결이 유휴 상태로 있을 수 있는 시간을 제한할 뿐, 연결이 얼마나 오래 살 수 있는지를 제한하지 않습니다.

왜 슬라이딩인가, 그리고 그 비용

이 서버가 발급하는 모든 토큰은 무상태입니다. 서명된 페이로드일 뿐이며, 그 이상도 아닙니다. 데이터베이스 행도, 세션 기록도, 다시 돌아왔을 때 조회할 항목도 없습니다. 이것이 Cloud Run 리비전 교체가 보이지 않게 만드는 이유입니다. 새 인스턴스는 이전 인스턴스가 발급한 토큰을 검증하며, 둘 사이에 공유 상태가 없습니다. 토큰 저장소가 있었다면 배포할 때마다 재연결이 필요했을 것입니다.

대가는 개별적으로 취소할 수 있는 것이 없다는 것입니다. 취소 엔드포인트가 없는 이유는 삭제할 것이 없기 때문입니다. 구체적으로:

  • 유출된 리프레시 토큰은 최대 30일 동안 액세스를 허용하며, 사용할 때마다 보유자의 액세스가 30일 더 연장됩니다. 무효화할 서버 측 기록이 없고, 도난당한 리프레시 토큰과 정당한 리프레시 토큰을 구별할 방법도 없습니다. 둘 다 같은 키로 서명된 같은 바이트이기 때문입니다.

  • 윈도우를 슬라이딩하는 것은 회전이 아닙니다. 리프레시가 새 리프레시 토큰을 발급하면, 교체된 이전 토큰은 그 안에 찍힌 만료 시각까지 계속 작동합니다. 진짜 회전은 이전 토큰을 사용된 것으로 표시하는 것을 의미하며, 이 설계에는 없는 저장소가 필요합니다.

  • 액세스 토큰도 같은 이유로 1시간 안에는 차단할 수 없습니다.

남은 것은 하나의 단순한 수단이며 즉시 효과가 있습니다. MCP_TOKEN_SIGNING_KEY를 변경하고 다시 배포하세요. 모든 액세스 토큰, 모든 리프레시 토큰, 열려 있는 모든 동의 페이지가 한 번에 무효화됩니다. 모두 그 키로 검증되기 때문입니다. 다음 Claude 요청은 401을 받고 운영자는 승인을 한 번 클릭합니다. 키를 정기적으로 회전하는 것 자체가 합리적인 정책입니다.

동의 화면은 어쨌든 누구도 인증하지 않습니다. 버튼 하나만 있고 비밀번호는 없습니다. 낯선 사람과 여러분의 노트북 사이를 막는 것은 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.tsexchangeRefreshToken에서 한 줄:

// 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. src/oauth-router.ts에서 SCOPES_SUPPORTED를 비웁니다. Claude는 메타데이터가 그것을 광고할 때만 인증 요청에 offline_access를 추가하며, 그것이 리프레시 토큰을 요청할지 결정하는 스위치입니다.

  2. src/oauth-provider.ts에서 issueTokens가 반환하는 값에서 refresh_token 필드를 제거합니다. 현재는 요청된 스코프와 관계없이 발급됩니다.

이 설정은 사용 중에 눈에 띌 것입니다. 1시간이 지나면 Claude가 세션 중간에 브라우저를 동의 화면으로 되돌려 보냅니다.

액세스 토큰 수명 줄이기. src/oauth-provider.tsACCESS_TOKEN_TTL_S는 유출된 액세스 토큰이 작동하는 기간을 좁힙니다. 만료 때마다 토큰 요청 한 번이 들고 사람의 개입은 전혀 없으므로 비용이 낮습니다. 하지만 유출된 리프레시 토큰에 대해서는 아무것도 하지 못합니다. 리프레시 토큰이야말로 걱정할 가치가 있는 자격 증명입니다.

Keepalive

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 헤더에 비밀 값을 제시하며, 어떤 작업도 수행되기 전에 상수 시간으로 비교됩니다. 이것은 Bearer 토큰이 아니라 공유 비밀입니다. 스케줄러는 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=3

90일 윈도우에 대해 주 1회면 충분하며 여러 번 누락된 실행을 견딜 여유가 있습니다. 이 작업은 토큰 엔드포인트 왕복 한 번과 Firestore 쓰기 한 번의 비용이 듭니다.

상태

의미

스케줄러가 해야 할 일

200

리프레시 토큰이 교환되었고 새 토큰이 저장됨

없음

401

비밀 값이 없거나 잘못됨

작업을 수정할 것; 라우트는 아무 작업도 하지 않음

404

서비스에 MCP_KEEPALIVE_SECRET이 설정되지 않음

설정하고 다시 배포

503 with "retryable": true

Firestore에 도달할 수 없음

재시도

503 with "retryable": false

grant가 더 이상 유효하지 않음

npm run bootstrap

이것이 보호하지 못하는 것: 조건부 액세스 로그인 빈도 정책, 비밀번호 변경, MFA 재설정, 또는 관리자가 grant를 철회하는 경우입니다. 이러한 것 중 하나라도 발생하면 스케줄이 무엇을 말하든 리프레시 토큰이 무효화되며, 코드 변경으로 피할 수 없습니다. Entra 테넌트가 여러분의 것이라면 이 앱 등록을 로그인 빈도 정책에서 제외하십시오. 그렇지 않다면 90일을 다른 사람이 알리지 않고 단축할 수 있는 상한선으로 취급하십시오.

keepalive 라우트는 아래 토큰 수명의 Layer-1 30일 윈도우와도 무관합니다. 해당 리프레시 토큰은 Claude의 커넥터 저장소에 있으며, 오직 Claude만 그것을 제시하거나 교체 토큰을 받을 수 있습니다. 따라서 여기서 실행되는 어떤 것도 그것을 유지할 수 없습니다. 그것을 잃으면 승인 버튼 클릭 한 번이 필요하고, Microsoft 토큰을 잃으면 디바이스 코드 로그인이 필요합니다.

알림

두 가지 실패는 로그 기반 메트릭 없이는 보이지 않습니다. 둘 다 Claude 대화 안의 메시지로만 나타나거나 아무도 읽지 않는 로그 한 줄로만 나타나기 때문입니다.

이벤트

의미

graph-auth-failure with retryable: "false"

Microsoft grant가 더 이상 유효하지 않습니다. 누군가 npm run bootstrap을 실행해야 합니다.

token-cache-write-refused

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"'

그런 다음 해당 메트릭이 0보다 클 때의 알림 정책을 설정합니다. 동의 승인도 주시할 가치가 있습니다. POST /consent가 302로 응답하는 일은 커넥터를 추가할 때만 발생해야 하며, 요청 로그에 이미 그 내용이 남습니다.

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

Bootstrap

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 bootstrap

이 스크립트는 Microsoft의 디바이스 코드 메시지를 출력하고, 브라우저에서 승인할 때까지 기다린 다음, 노트북을 한 번 나열하고 개수를 출력하여 토큰이 작동함을 증명합니다. 마지막 줄에는 기록된 Firestore 프로젝트와 문서, 그리고 계정의 홈 테넌트 이름이 나와서 올바른 디렉터리로 로그인했는지 확인할 수 있습니다. 이 출력에는 테넌트 ID가 포함되므로 이슈, 풀 리퀘스트, 워크플로 로그에 남기지 마십시오.

GOOGLE_CLOUD_PROJECTFIRESTORE_CACHE_DOC여기서 필수입니다. 서버에서는 전자가 추론되고 후자가 기본값을 가지지만, 여기서는 그렇지 않습니다. CLI는 여러분 자신의 Application Default Credentials로 쓰기 때문에, 값이 설정되지 않으면 gcloud 로그인이 가리키는 프로젝트에 실제 문서를 만들고 여전히 성공 메시지를 출력할 것입니다. MCP_OAUTH_* 값은 읽지 않으므로 이 명령을 실행해도 Layer-1 클라이언트 비밀이 여러분의 머신에 들어오지 않습니다.

서버 로그에 retryable: "false"가 포함된 graph-auth-failure 이벤트가 나타날 때마다 이 명령을 다시 실행하십시오. 리프레시 토큰은 사용할 때마다 회전되며, 서비스가 약 90일 이상 유휴 상태로 있으면 죽습니다. 자동 복구는 없습니다. 위의 keepalive 작업을 구성하는 것이 유휴 상태가 그러한 상황에 이르는 경로 중 하나가 되는 것을 막는 방법입니다.

컨테이너

서비스는 linux/amd64를 실행하는 Cloud Run에 배포됩니다. 이미지는 해당 플랫폼용으로 명시적으로 빌드되어 @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이 이미 사용 중이면 다른 포트로 매핑하십시오. -e PORT=8080과 함께 -p 8081:8080을 사용하세요.

이미지를 테스트하려면:

RUN_DOCKER_TESTS=1 bash scripts/test/run.sh

이 명령은 이미지를 빌드하고, resvg glibc 바이너리가 프로덕션 전용 설치 과정에서 살아남았는지, dev 의존성이 함께 들어오지 않았는지 확인하며, 컨테이너 안에서 SVG를 PNG로 렌더링하고 /healthzPORT에 지정된 포트에서 200으로 응답하는지 검증합니다. RUN_DOCKER_TESTS=1이 없으면 docker 스위트는 건너뛰고 나머지는 계속 실행됩니다.

배포

.github/workflows/deploy.ymlmain에 푸시될 때마다, 그리고 workflow_dispatch로도 실행됩니다. 타입 검사를 수행하고, npm test를 실행하고, 빌드하며, commit sha로 태그된 컨테이너 이미지를 빌드하여 Artifact Registry에 푸시하고, 그 이미지를 Cloud Run에 배포합니다. 타입 검사나 테스트가 실패하면 이미지가 빌드되기 전에 실행이 중단됩니다.

GitHub에는 장기(long-lived) 자격 증명이 존재하지 않습니다. Job은 Workload Identity Federation을 통해 인증합니다. permissions: id-token: write가 GitHub OIDC 토큰을 요청할 수 있게 해 주고, google-github-actions/auth@v2가 그 토큰을 수명이 짧은 Google 자격 증명으로 교환합니다. scripts/gcp-bootstrap.sh는 서비스 계정 JSON 키를 생성하지 않으며, 어디에서도 그런 키가 필요하지 않습니다. Provider는 repository 클레임이 이 저장소인 토큰만 허용합니다.

이미지는 ubuntu-latest에서 빌드되는데, 이는 Cloud Run이 실행되는 플랫폼이자 @resvg/resvg-js 프리빌드(prebuild)가 컴파일된 플랫폼인 linux/amd64입니다. 그래서 워크플로우는 gcloud run deploy --source를 사용하는 대신 이미지를 직접 빌드합니다. 후자를 사용하면 Cloud Build를 활성화하고 그에 수반되는 역할들을 부여해야 하기 때문입니다.

배포는 런타임 서비스 계정으로서 --max-instances=1--allow-unauthenticated로 실행되며, 이 계정은 Firestore 토큰 캐시에 대한 roles/datastore.user를 보유합니다. --allow-unauthenticated가 있어야 Claude가 서비스에 아예 도달할 수 있습니다. MCP 엔드포인트는 그 대신 Bearer 토큰을 통해 잠겨 있습니다. MCP 엔드포인트에서의 Bearer 토큰 참고.

저장소가 보유해야 하는 값

scripts/bootstrap.sh는 GCP 쪽을 프로비저닝하고, 처음 여섯 개 항목에 대한 gh variable set 명령을 출력합니다. 워크플로우는 절반만 구성된 상태로 배포하는 대신, 무엇이 누락되었는지 밝히면서 첫 단계에서 실패합니다.

이름

종류

GCP_PROJECT

변수

프로젝트 ID

GCP_REGION

변수

Cloud Run 리전

GAR_REGION

variable

Artifact Registry 리전

WIF_PROVIDER

variable

전체 workload identity provider 리소스 이름

DEPLOY_SA

variable

배포 서비스 계정 이메일

RUNTIME_SA

variable

런타임 서비스 계정 이메일

ONENOTE_CLIENT_ID

variable

Azure 앱 등록 클라이언트 ID

ONENOTE_AUTHORITY

variable

Entra authority URL

MCP_OAUTH_CLIENT_ID

variable

Layer-1 OAuth 클라이언트 ID

MCP_PUBLIC_URL

variable

서비스의 공개 URL. 아래 참고

FIRESTORE_CACHE_DOC

variable

선택 사항. 기본값은 tokencache/msal

MCP_OAUTH_CLIENT_SECRET

secret

Layer-1 OAuth 클라이언트 시크릿

MCP_TOKEN_SIGNING_KEY

secret

액세스 토큰 서명 키, 32자 이상

MCP_KEEPALIVE_SECRET

secret

선택 사항. 설정하지 않으면 POST /keepalive는 마운트되지 않음

이 중 실제 자격 증명은 세 개뿐입니다. WIF 프로바이더 이름과 서비스 계정 이메일은 식별자에 불과하며, 이 저장소의 OIDC 신원을 제시할 수 없는 사람에게는 쓸모가 없으므로 비밀이 아닌 변수로 취급합니다.

배포는 env_vars_update_strategy: overwrite를 전달합니다. 따라서 워크플로우에 적힌 목록은 모든 배포 리비전에서 서비스의 전체 환경 변수입니다. 이 액션의 기본값은 merge인데, 이 경우 워크플로우에서 제거된 변수가 이전 리비전에서 조용히 남게 됩니다. PORTGOOGLE_CLOUD_PROJECT는 의도적으로 목록에 넣지 않았습니다. 둘 다 Cloud Run이 제공하며, PORT는 입력으로 전달하면 거부되기 때문입니다.

첫 배포와 MCP_PUBLIC_URL

MCP_PUBLIC_URL이번 서버가 발급하는 모든 액세스 토큰의 OAuth 발급자(issuer)이자 대상(audience)입니다. 첫 배포가 일어나기 전에는 URL을 가진 서비스가 존재하지 않습니다. 워크플로우는 이 값을 세 단계로 해결합니다. MCP_PUBLIC_URL 저장소 변수 → 그다음 Cloud Run이 서비스에 이미 배정한 URL → 그다음 — 둘 다 없을 때만 — https://placeholder.invalid이며, 이 값을 배포 직후 실제 URL로 대체합니다. 따라서 첫 실행은 무인으로 동작하고 올바른 값이 갖춰진 상태로 끝납니다. 그리고 URL을 명시하는 경고를 남깁니다. 그 저장소 변수를 해당 URL로 설정하세요. 서비스 앞에 커스텀 도메인을 둔 뒤에도 세 가지 소스 중 유일하게 저장소 변수만 살아남기 때문입니다.

롤백

이미지 태그가 커밋 sha이므로 이전 이미지는 여전히 Artifact Registry에 남아 있습니다.

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

workflow_dispatch로 이전 커밋에서 워크플로우를 다시 실행하는 것도 유효하며, 이 방법이 배포된 환경을 해당 커밋의 워크플로우 파일과 맞춰 유지합니다.

설정

모든 값은 환경 변수에서 가져오며 시작 시 검증됩니다. 누락되거나 잘못된 형식의 변수가 있으면 무엇이 잘못되었는지 한 번에 모두 나열하는 ConfigError가 발생하고, 프로세스는 스택 트레이스 없이 종료 코드 1로 종료됩니다.

변수

필수

기본값

용도

ONENOTE_CLIENT_ID

Azure 앱 등록 클라이언트 ID(퍼블릭 클라이언트)

ONENOTE_AUTHORITY

테넌트용 Entra ID authority URL

MCP_OAUTH_CLIENT_ID

Claude가 먼저 제시하는 Layer-1 OAuth 클라이언트 ID

MCP_OAUTH_CLIENT_SECRET

Layer-1 OAuth 클라이언트 시크릿

MCP_TOKEN_SIGNING_KEY

발급된 액세스 토큰 서명에 쓰는 키(최소 32자)

MCP_PUBLIC_URL

서비스 자체의 공개 URL: https, 쿼리 없음, 프래그먼트 없음, 후행 슬래시 없음

FIRESTORE_CACHE_DOC

서버: 아니오 · 부트스트랩:

tokencache/msal

MSAL 토큰 캐시를 보관하는 Firestore 문서 경로

GOOGLE_CLOUD_PROJECT

서버: 아니오 · 부트스트랩:

GCP 프로젝트. Cloud Run에서 자동으로 추론됨

PORT

아니오

8080

바인딩 포트. Cloud Run이 지정하며 서버는 절대 하드코딩하지 않음

MCP_KEEPALIVE_SECRET

아니오

최소 32자. 설정하면 POST /keepalive가 마운트되고, 설정하지 않으면 404를 반환. Keepalive 참고.

FIRESTORE_CACHE_DOCsrc/token-cache.ts에 있는 MSAL 캐시 플러그인이 읽고 쓰는 문서를 뜻합니다. 해당 값은 문서 경로여야 하며, 즉 슬래시로 구분된 세그먼트 개수가 짝수여야 합니다. loadConfig는 시작 시 컬렉션 경로를 거부합니다.

ONENOTE_CLIENT_IDONENOTE_AUTHORITYsrc/graph-auth.ts가 Entra ID에 제시하는 Azure 앱 등록 정보를 식별합니다. 이 앱 등록은 퍼블릭 클라이언트이므로 의도적으로 Layer-2 클라이언트 시크릿이 없습니다. 뒤따르는 MCP_OAUTH_* 값들은 Claude-서버 사이의 Layer-1에 속하며 전혀 관계가 없습니다.

MCP_PUBLIC_URL은 Claude가 이 서비스에 접근하는 URL입니다. OAuth 발급자, 액세스 토큰이 묶일 resource 식별자, 그리고 보호된 리소스 메타데이터 문서의 URL이 모두 이 값에서 만들어집니다. Cloud Run은 프로세스에 어떤 URL로 접근되는지 알려주지 않으며, Host 헤더에서 가져온 값은 호출자가 보낸 그대로 어떤 값이든 될 수 있으므로, 해당 URL을 환경 변수로 설정합니다. 첫 배포가 URL을 만들어낸 후이어야 채울 수 있는 값입니다.

npm run bootstrap 명령은 ONENOTE_CLIENT_ID, ONENOTE_AUTHORITY, FIRESTORAGE_CACHE_DOC, GOOGLE_CLOUD_PROJECT만 읽습니다. MCP_OAUTH_*는 읽지 않으며, 마지막 두 개는 기본값을 두지 않고 요구합니다. Bootstrap 참고.

저장소 청결 유지

이 저장소는 공개 저장소입니다. 실제 페이지 콘텐츠, 친구로 렌더링된 잉크, Entra 테넌트 이름이나 ID, Firestore 문서 내용을 커밋하지 마세요. .gitignoreoutput/과 토큰 캐시 파일 패턴을 제외한다. project-spec.md의 "Repo hygiene" 섹션을 참고하세요.

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

Maintenance

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

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

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

Related MCP Servers

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

View all related MCP servers

Related MCP Connectors

  • Microsoft OneNote (Microsoft 365) MCP Pack

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

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

View all MCP Connectors

Latest Blog Posts

MCP directory API

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

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

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