본문 바로가기
AI.IT

Claude 커스텀 MCP, 개인 기억 시스템을 연결해본 기록

by bamsik 2026. 5. 21.
반응형
AI · MCP · MEMORY

Claude 커스텀 MCP로 개인 기억 시스템을 연결해본 기록

Claude Custom Connector에 개인 기억 MCP를 붙이면서 겪은 인증 구조, OAuth 흐름, 그리고 여러 AI 도구가 같은 기억층을 공유하게 만드는 과정을 정리했다.

핵심 결론

MCP 서버 URL에 비밀 토큰을 박는 방식이 아니라, 토큰 없는 /mcp 리소스를 OAuth Bearer token으로 보호해야 Claude Custom Connector 흐름과 맞았다.

Claude 커스텀 MCP를 만들면서 제일 크게 느낀 건, 이제 AI 도구도 각자 따로 노는 메모리가 아니라 하나의 개인 기억 시스템을 공유해야 한다는 점이었다. 이번 작업은 단순히 MCP 서버 하나 붙이는 일이 아니었다. 개인 메모리, Wiki, 터미널, Claude 앱, OpenClaw, Hermes가 같은 기억을 읽고 저장하도록 묶는 작업이었다.

왜 개인 기억 시스템부터 만들었나

AI 도구를 오래 쓰다 보면 가장 먼저 부딪히는 문제가 있다. 모델이 똑똑한지는 둘째 치고, 대화창이 바뀔 때마다 나를 처음 만난 사람처럼 대한다는 점이다.

어제 정리한 프로젝트 맥락, 지난주에 해결한 에러, 자주 쓰는 서버 구성, 내가 선호하는 작업 방식이 도구마다 흩어진다. Claude 앱에서 말한 내용은 터미널 에이전트가 모르고, 터미널에서 저장한 기록은 Claude 앱이 모른다. 별도로 관리하는 Wiki 문서는 또 직접 찾아야 한다.

그래서 이번에 만들고 싶었던 건 특정 앱 하나의 메모리가 아니었다. 도구들이 같이 쓰는 개인 기억층이었다. 터미널에서 저장한 기억을 Claude 앱에서도 검색하고, Claude 앱에서 저장한 기억을 Hermes나 OpenClaw에서도 이어서 쓰는 구조. 사람이 여러 노트 앱을 쓰더라도 머릿속 맥락은 하나인 것처럼, AI 도구들도 같은 기억 기반 위에서 움직이게 하고 싶었다.

구조는 세 층으로 잡았다.

  • 첫 번째는 개인 메모리 저장소다. 프로젝트 맥락, 결정 사항, 에러 해결 기록, 반복되는 작업 방식을 저장한다.
  • 두 번째는 Wiki 레이어다. 오래 유지할 내용은 주제별 문서로 정리해서 사람도 읽을 수 있게 둔다.
  • 세 번째는 MCP 서버다. Claude 앱이나 다른 클라이언트가 같은 기억 저장소에 접근하도록 연결한다.

이렇게 해두면 Claude에게 “전에 이 문제 어떻게 해결했지?”라고 물었을 때, Claude가 MCP를 통해 개인 기억 저장소를 검색할 수 있다. 반대로 중요한 결정이 생기면 Claude가 직접 기억 저장 도구를 호출할 수도 있다.

처음에는 MCP 서버만 만들면 끝일 줄 알았다

처음 계획은 꽤 단순했다. 이미 로컬에 기억 저장 시스템이 있으니, 여기에 MCP 서버를 붙이면 된다. 기억 검색, 기억 저장, 최근 기억 조회, 단건 조회 정도만 MCP tool로 감싸면 Claude Custom Connector에서 바로 쓸 수 있을 거라고 봤다.

로컬에서는 실제로 그렇게 됐다. Node.js로 Streamable HTTP MCP 서버를 띄우고, 기존 기억 시스템의 함수를 MCP tool로 연결했다. initialize도 되고, tools/list도 됐다. 여기까지는 생각보다 순조로웠다.

문제는 Claude 앱에서 원격 MCP 서버로 붙는 순간부터 시작됐다. Claude Custom Connector는 내 로컬 프로세스에 직접 붙는 게 아니라, Claude 쪽 서버에서 인터넷을 통해 MCP 서버에 접근한다. 그래서 로컬 서버를 외부에서 접근할 수 있게 터널 뒤에 올려야 했다.

여기까지도 예상한 일이었다. 진짜 삽질은 인증에서 시작됐다.

외부에 노출되는 MCP 서버니까 아무나 접근하면 안 된다. 처음에는 가장 간단한 방법으로 URL 경로에 긴 토큰을 넣었다. 예를 들면 이런 구조다.

https://example.com/private-random-path/mcp

개인용 비공개 도구에서는 이런 방식을 종종 쓴다. 토큰이 충분히 길고 예측하기 어렵다면 비밀 URL처럼 쓸 수 있고, 헤더를 직접 넣기 어려운 클라이언트에서도 편하다. 실제로 로컬 테스트에서는 잘 됐다. MCP initialize도 되고, 도구 목록도 나오고, 호출도 됐다.

그런데 Claude Custom Connector에서는 계속 실패했다. 처음에는 서버에 도달하지 못하는 문제처럼 보였다. 그래서 health check를 만들고, GET 요청을 받아주고, SSE probe도 처리했다. Claude가 보내는 Accept 헤더를 로그로 보고, text/event-stream과 application/json 처리도 맞췄다.

조금씩 앞으로 가는 듯했다. 처음에는 “서버에 도달할 수 없음” 계열의 오류가 났고, 그다음에는 OAuth 흐름이 시작되는 데까지 갔다. 그런데 마지막에 또 실패했다.

Authorization with the MCP server failed.

서버 로그를 보면 더 헷갈렸다. register는 성공했다. authorize도 성공했다. token도 성공했다. 그런데 Claude UI에서는 인증 실패라고 했다. 서버 입장에서는 “나는 토큰을 발급했는데?”가 되고, Claude 입장에서는 “이 토큰으로 이 리소스에 접근하는 구조가 이상한데?”가 된 셈이다.

답은 OAuth 구조를 잘못 잡은 데 있었다

결국 MCP authorization 문서를 다시 읽었다. MCP Authorization 문서Anthropic MCP 문서를 다시 보니, 내가 만든 구조가 Claude가 기대하는 방식과 달랐다.

핵심은 이거였다. MCP 서버는 OAuth 관점에서 Resource Server다. Authorization Server가 access token을 발급하고, MCP client는 이후 MCP 요청마다 Bearer 헤더를 붙여야 한다.

Authorization: Bearer access_token

그리고 미인증 요청에 대해서는 MCP 서버가 401 Unauthorized를 반환하면서 WWW-Authenticate 헤더로 protected resource metadata 위치를 알려줘야 한다.

HTTP/1.1 401 Unauthorized WWW-Authenticate: Bearer resource_metadata="https://example.com/.well-known/oauth-protected-resource/mcp"

여기서 중요했던 건 Claude에 등록하는 MCP URL이다. Claude에는 비밀 토큰이 박힌 URL이 아니라 보호된 리소스의 표준 URL을 넣어야 했다.

https://example.com/mcp

인증은 URL이 아니라 OAuth access token으로 처리해야 했다. 처음에 만든 path-token 방식은 개인 진단용으로는 쓸 수 있지만, Claude Custom Connector의 OAuth 흐름과는 맞지 않았다. Claude는 MCP 서버 URL을 OAuth resource 값으로 해석한다. 여기에 비밀 토큰이 들어가면 토큰이 박힌 URL 자체가 리소스 식별자가 된다. 그게 문제였다.

그래서 구조를 바꿨다. Claude에는 토큰 없는 MCP URL만 등록한다. 미인증 상태로 /mcp에 접근하면 서버는 401을 반환한다. 이때 WWW-Authenticate 헤더를 꼭 넣는다. 그다음 Claude가 metadata를 읽고 OAuth 흐름을 시작한다.

최종 흐름은 이렇게 정리됐다.

  • Claude가 /mcp에 접근한다.
  • 서버가 401과 WWW-Authenticate 헤더를 반환한다.
  • Claude가 protected resource metadata를 조회한다.
  • Claude가 authorization server metadata를 조회한다.
  • Claude가 dynamic client registration을 수행한다.
  • Claude가 authorize endpoint를 호출한다.
  • 서버가 authorization code를 callback으로 돌려준다.
  • Claude가 token endpoint에서 access token을 받는다.
  • 이후 Claude는 /mcp 요청마다 Authorization: Bearer 헤더를 사용한다.

이 구조로 바꾸고 나서야 Claude Custom Connector 연결이 성공했다.

오늘 제일 힘들었던 부분

이번 작업이 피곤했던 이유는 문제가 한 군데에 있지 않았기 때문이다. 처음에는 터널 문제처럼 보였다. 그다음에는 MCP transport 문제처럼 보였다. 그다음에는 Accept 헤더 문제처럼 보였다. 마지막에는 OAuth token response 문제처럼 보였다.

실제로는 이 모든 게 조금씩 얽혀 있었다.

Claude 쪽 클라이언트는 연결 과정에서 여러 종류의 요청을 보냈다. 어떤 요청은 진짜 MCP JSON-RPC 요청이고, 어떤 요청은 단순 도달성 확인처럼 보였다. Accept 헤더도 매번 같지 않았다. application/json을 보내는 요청도 있었고, text/event-stream을 기대하는 요청도 있었고, 그냥 */*로 오는 요청도 있었다.

Streamable HTTP MCP 서버는 Accept 헤더가 맞지 않으면 406을 낼 수 있다. 그런데 Claude가 */*로 POST를 보내는 경우가 있었다. 이걸 그대로 두면 서버는 스펙에 맞게 거절하지만, Claude 입장에서는 연결 실패처럼 보일 수 있다. 그래서 서버에서 Accept 헤더를 application/json, text/event-stream으로 보정했다.

또 Claude UI가 GET 요청으로 MCP URL을 한번 찔러보는 경우도 있었다. 이때 무조건 405나 406을 내면 서버 도달 실패로 판단될 수 있다. 그래서 인증된 GET 요청에는 짧은 reachability 응답이나 SSE heartbeat를 돌려주는 식으로 처리했다.

가장 헷갈렸던 건 OAuth가 중간까지 성공한다는 점이었다. 완전히 안 되면 원인을 좁히기 쉽다. 그런데 register, authorize, token이 모두 성공으로 찍히는데 UI에서는 Authorization failed가 떴다. 이 상태가 제일 사람을 지치게 한다.

결국 로그를 세세하게 남긴 게 도움이 됐다. 요청 method, path, status, Accept 헤더, user-agent를 모두 남겨두지 않았다면 어디서 막혔는지 알기 어려웠을 것이다. Claude 쪽 오류 메시지는 꽤 추상적이다. “Authorization failed”만 보고는 token response가 문제인지, Bearer 사용이 문제인지, resource URI가 문제인지 알 수 없다.

마지막에 성공 로그가 찍혔을 때 흐름은 명확했다. Claude가 먼저 /mcp에 접근해서 401을 받았다. 그다음 metadata, register, authorize, token을 거쳤고, 마지막에는 Claude-User가 /mcp로 200과 202 응답을 받았다. 이때야 비로소 “이제 연결됐다”는 느낌이 왔다.

MCP AUTH FLOW
/mcp 요청
401 + metadata
register / authorize
Bearer token
MCP 연결

결국 얻은 것

작업이 끝난 뒤에는 Claude 앱에서 개인 기억 MCP를 연결할 수 있게 됐다. 이제 Claude는 같은 기억 저장소에 접근한다. 터미널에서 저장한 기억도 Claude가 검색할 수 있고, Claude에서 저장한 기억도 Hermes나 OpenClaw 같은 다른 에이전트 환경에서 이어서 쓸 수 있다.

이게 이번 작업의 핵심이었다. 특정 앱 하나의 메모리가 아니라, 여러 도구가 공유하는 개인 기억층을 만든 것.

예를 들면 이런 식이다. 터미널에서 작업하다가 에러 해결 과정을 기억에 저장한다. 나중에 Claude 앱에서 비슷한 문제를 물어보면 그 기록을 검색한다. Hermes는 같은 기억을 읽고 현재 작업 맥락을 이어간다. OpenClaw 쪽 자동화도 같은 저장소를 쓴다. 오래 유지할 내용은 Wiki 문서로 정리되어 사람도 다시 읽을 수 있다.

이 구조가 안정되면 AI 도구를 바꿔도 맥락을 잃지 않는다. 모델이 바뀌고, 앱이 바뀌고, 터미널 세션이 바뀌어도 기억은 남는다.

요즘 AI 도구는 대부분 대화를 중심으로 설계되어 있다. 하지만 실제 작업은 대화 단위로 끝나지 않는다. 프로젝트는 며칠, 몇 주, 몇 달씩 이어진다. 에러 해결 기록도 쌓인다. 개인 취향도 쌓인다. 한 번 정리한 인프라 구조를 매번 다시 설명하고 싶지 않다.

그래서 AI에게 필요한 건 단순한 채팅 기록이 아니라, 도구를 넘나드는 기억 시스템이라고 생각한다. Claude가 편할 때는 Claude를 쓰고, 터미널 에이전트가 편할 때는 터미널을 쓰고, 자동화는 별도 에이전트에게 맡기더라도 모두가 같은 기억을 보고 있으면 작업의 연속성이 생긴다.

이번 MCP 작업은 그 기반을 만드는 일이었다. 기억을 앱에 넣는 게 아니라, 앱들이 같은 기억을 보게 만든다. 그게 이번 Claude Custom MCP 작업에서 가장 중요했던 결론이다.

다음에 비슷한 작업을 한다면

다음에 원격 MCP 서버를 다시 만든다면 처음부터 이렇게 할 것 같다.

  • Claude에 등록할 URL은 토큰 없는 /mcp로 둔다.
  • 미인증 /mcp는 401과 WWW-Authenticate 헤더를 정확히 반환한다.
  • OAuth protected resource metadata와 authorization server metadata를 먼저 구현한다.
  • Dynamic Client Registration을 지원한다.
  • Access token은 URL이 아니라 Authorization: Bearer 헤더로만 받는다.
  • Path-token 방식은 임시 진단용으로만 남긴다.
  • 요청 로그에는 method, path, status, Accept, user-agent를 꼭 남긴다.

그리고 무엇보다 문서를 처음부터 OAuth Resource Server 관점으로 읽을 것 같다. 처음에는 MCP tool을 노출하는 서버라고만 생각했는데, 원격 MCP에서는 인증 서버와 리소스 서버의 관계를 제대로 이해해야 했다.

이번 작업은 보기보다 오래 걸렸다. MCP 서버 하나 붙이는 일이라고 생각했는데, 실제로는 개인 메모리 시스템, Wiki 정리 방식, 원격 MCP transport, 터널, OAuth 2.1 흐름, Claude Custom Connector의 실제 동작까지 한꺼번에 맞추는 일이었다.

그래도 결과는 만족스럽다. 이제 기억은 특정 앱 안에 갇혀 있지 않다. 터미널에서도, Claude 앱에서도, OpenClaw에서도, Hermes에서도 같은 기억을 읽고 저장할 수 있는 기반이 생겼다.

AI 도구를 더 많이 쓸수록 중요한 건 모델 자체보다도 맥락을 어떻게 유지할 것인가가 된다. 이번 작업은 그 질문에 대한 내 나름의 답이었다.

반응형