본문으로 건너뛰기

© 2026 Molayo

Qiita헤드라인2026. 06. 20. 09:55

MCP로 만드는 AI 에이전트 기억 서버: 실운영에서 정립된 설계 패턴 3가지 (TypeScript 구현 포함)

요약

MCP(Model Context Protocol)를 활용하여 여러 AI 에이전트가 공유할 수 있는 기억 서버를 설계하는 3가지 패턴을 소개합니다. TypeScript와 MCP SDK를 사용하여 실운영 환경에서 검증된 파일 기반의 기억 저장소 구현 방법을 다룹니다.

핵심 포인트

  • MCP 서버를 통해 에이전트 간 독립적인 기억 공유 가능
  • DB 대신 공유 디렉터리의 Markdown 파일을 활용한 스토리지 설계
  • 관리 효율을 위한 '1파일 1사실' 저장 패턴 권장
  • 벡터 DB 없이 description의 품질만으로도 효과적인 검색 가능

지난 기사에서는 AI 에이전트의 장기 기억을 「Markdown 파일 + git」으로 구현하는 방법을 작성했습니다. 이번에는 그 후속편으로, 기억 저장소(memory store)를 MCP (Model Context Protocol) 서버로 분리하는 설계를 다룹니다.

MCP화하려는 동기는 하나입니다. 우리는 여러 AI 에이전트(블로그 담당, QC 담당, 배포 담당)를 운용하고 있으며, 기억을 특정 클라이언트 구현에 가두고 싶지 않았습니다. MCP 서버로 만들어 두면 Claude Code, Claude Desktop, 자체 제작 API 클라이언트 등 어디에서든 동일한 기억을 읽고 쓸 수 있습니다. 모델이나 도구를 교체하더라도 기억은 자산으로서 남습니다.

이 기사에서는 6개월간의 실운영을 통해 도태되고 살아남은 3가지 설계 패턴을 동작하는 TypeScript 코드로 해설합니다.

  • 패턴 1: 1파일 1사실 저장소 (1 file 1 fact store)
  • 패턴 2: 상태 머신(State Machine)이 포함된 기억
  • 패턴 3: 인수인계 장부 (handoff ledger)

먼저 토대입니다. 공식 SDK @modelcontextprotocol/sdk를 사용합니다.

mkdir agent-memory-mcp && cd agent-memory-mcp
npm init -y
npm install @modelcontextprotocol/sdk zod
...
// src/index.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
...

Claude Code 측의 등록은 한 줄입니다.

claude mcp add agent-memory -e MEMORY_DIR=/srv/team-memory -- node dist/index.js

여기서 첫 번째 운용 지견을 하나 공유합니다. 스토리지(Storage)는 공유 디렉터리 위의 플레인 텍스트(plain text)로 한다. 여러 에이전트가 동일한 기억을 볼 때, DB 서버를 세우는 것보다 「모두가 읽을 수 있는 디렉터리의 Markdown」이 장애 조사나 리뷰를 git 차이(diff)로 할 수 있어 압도적으로 편했습니다.

첫 번째 패턴은 기억의 최소 단위 설계입니다. 원칙은 "1파일 = 1사실"입니다.

안티 패턴(Anti-pattern)은 「learnings.md에 계속 추가하는」 방식입니다. 저희도 처음에는 이 방식을 사용했다가, 3주 만에 아무도 전체를 읽지 않는 2,000행짜리 파일이 탄생했습니다. 1파일 1사실로 분할하면 갱신, 폐기, 검색, 리뷰의 단위가 일치하게 됩니다.

const StoreInput = {
name: z.string().regex(/^[a-z0-9-]+$/).describe("kebab-case 슬러그"),
description: z.string().max(200).describe("1행 요약. 검색은 이 행을 대상으로 함"),
...

recall(읽기)은 frontmatter의 description 행에 대한 정규 표현식 매칭입니다. 지난번에도 썼지만, 기억이 수백 건 규모라면 벡터 DB(Vector DB)는 필요 없으며, description의 어휘 품질이 검색 정밀도에 더 효과적입니다.

server.registerTool(
"memory_recall",
{
...

description에 「작업 착수 전에 반드시 호출할 것」이라고 적혀 있는 것은 장식이 아닙니다. 실운영에서 가장 많았던 사고는 "기억은 존재했지만, AI가 읽지 않고 작업을 수행한 것"입니다. 소실된 스크립트를 복구할 때, 정본 사양이 기억에 남아 있었음에도 참조되지 않아 열화된 버전이 재구현되었습니다. tool description은 실질적으로 시스템 프롬프트(System Prompt)의 일부이므로, 호출 타이밍에 대한 규율은 여기에 작성합니다.

두 번째 패턴은 기억의 신뢰도 관리입니다. 기억에는 **라이프사이클(Lifecycle)**이 있습니다.

draft(가설) ──검증──▶ confirmed(확정) ──진부화──▶ deprecated(폐기)

이를 구분하지 않으면 어떤 일이 벌어질까요. 어떤 에이전트가 대화 중에 세운 가설을 저장하고, 다른 에이전트가 그것을 확정 사항으로 참조하여 작업을 진행하는 사고가 발생합니다. 저희의 경우, 일단 철회된 설계 방침이 「결정 사항」으로서 독자적으로 움직여 수 시간의 재작업(rework)이 발생했습니다.

규칙은 3가지입니다.

  • 신규 저장은 반드시 draft

로 들어가야 합니다 (패턴 1의 코드 참조).

  • confirmed로의 승격은 저장한 본인 이외의 대상(인간 또는 다른 에이전트)이 수행해야 합니다. 틀린 것으로 판명된 기억은 삭제하지 않고 deprecated 상태로 남겨둡니다. "왜 틀렸는가"가 다음 사고를 방지합니다.

승격 도구(promotion tool)의 구현입니다. 자기 승인 방지(self-approval guard)가 핵심입니다.

server.registerTool(
"memory_promote",
{
...

"AI가 자신이 작성한 기억을 스스로 확정할 수 없다" —— 단지 이 하나의 제약만으로도, 잘못된 확신이 고착화되는 현상이 눈에 띄게 줄었습니다. 인간의 코드 리뷰(code review)와 동일한 구조입니다.

세 번째는 지식이 아닌 **행동의 기록(record of actions)**입니다. 외부에 영향을 미치는 조작(기사 공개, 배포, API 실행)은 일반적인 기억과는 별도의 원장(ledger)에 기록합니다.

왜 분리하는가? 백업 복구 시 상태 파일(state file)이 되돌아갔을 때, 이미 공개된 기사를 "미공개"로 판정하여 스크립트가 같은 기사를 재공개하려고 시도하는 사고가 발생했기 때문입니다. 외부 조작의 기록은 "추억"이 아니라 "회계 기록"이며, 추가 전용(append-only)으로 설정하고 증적 URL을 필수로 합니다.

const LEDGER = path.join(MEMORY_DIR, "ledger.jsonl");
server.registerTool(
"ledger_record",
...

운영 규칙은 "check → 실행 → record를 한 세트"로 합니다. record를 잊으면 다음 날 또 같은 공개를 시도하며 루프에 빠집니다 (이것도 실제로 저질렀던 실수입니다. 원장에 기록을 누락한 단 한 건의 실수로 인해, cron이 3일 동안 같은 기사의 공개를 계속 재시도했습니다).

동시 쓰기 경합(write contention). 여러 에이전트가 동시에 원장에 쓰면 행(line)이 뒤섞일 수 있습니다. JSONL의 추가(append)는 한 행이 작다면 원자적(atomic)에 가깝지만, 엄격하게 처리하려면 proper-lockfile 등으로 락(lock)을 잡아야 합니다. 저희는 "외부 조작을 실행할 수 있는 에이전트를 한 명으로 제한한다"라는 운영 측면의 해결책을 선택했습니다. 기술로 해결하는 것보다 역할 분담으로 해결하는 것이 더 안정적인 경우도 있습니다.

스코프(scope)의 혼선. 모든 에이전트가 모든 기억을 읽을 수 있으면, 블로그 담당의 기억이 배포 담당의 판단을 오염시킬 수 있습니다. frontmatter에 scope:를 갖게 하고, 회상(recall) 시 에이전트 이름으로 필터링하는 것이 효과적이었습니다.

기억의 비대화. 인덱스나 description에 글자 수 상한(200자)을 기계적으로 부과합니다. "나중에 정리하겠다"는 결코 오지 않습니다. 쓰기 시점에 차단하는 것이 유일하게 작동하는 방법이었습니다.

패턴해결하는 문제핵심 제약
1파일 1사실비대화·검색성입도(granularity) 통일, description 200자
...

세 가지에 공통적인 것은, "기억을 똑똑하게 만드는 것"이 아니라 "기억의 신뢰도와 부작용을 관리하는 것"이라는 방향성입니다. MCP로 분리해 두면, 이러한 규율을 도구의 인터페이스로서 강제할 수 있다는 점 —— description과 가드 코드(guard code)를 통해 운영 규칙을 구현에 심을 수 있다는 점 —— 이 최대의 이점이었습니다.

코드는 모두 그대로 실행 가능합니다. MEMORY_DIR을 공유 스토리지로 설정하여, 꼭 여러 에이전트로 테스트해 보시기 바랍니다.

AI 자동 생성 콘텐츠

본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0