Skip to main content

2부 9장. 외부 시스템 붙이기

MCP 서버를 처음 만들면 대개 이런 기분이 듭니다. search_notes가 잘 되고, semantic_search_notes도 그럴듯하게 돌아가고, Inspector에서 메시지도 예쁘게 보입니다. 그런데 어느 순간 이런 생각이 듭니다.

"좋아, 이제 진짜 세상과 연결해보자."

바로 여기서 MCP의 성격이 드러납니다. MCP 서버의 진짜 가치는 내부 함수 몇 개를 잘 짜는 데 있지 않습니다. 데이터베이스, 기존 API, SaaS 서비스처럼 이미 돌아가고 있는 외부 세계를 얼마나 자연스럽게 연결하느냐에 있습니다. 잘 만든 MCP 서버는 새로운 시스템을 만드는 도구라기보다, 이미 존재하는 시스템들 사이에 통역사를 세우는 일에 가깝습니다.

이번 장에서는 PKM-MCP를 한 단계 더 현실적인 서버로 발전시킵니다. 먼저 SQLite와 PostgreSQL을 어떻게 붙일지 패턴을 익히고, 기존 REST API를 MCP 도구로 감싸는 법을 살펴본 다음, GitHub 이슈를 노트로 가져오는 도구를 추가해 보겠습니다. 이 작업이 끝나면 PKM-MCP는 단순한 메모 서버가 아니라, 외부 정보를 받아들이고 내부 지식으로 흡수하는 CRUD+통합 서버가 됩니다.

2부 9장. 외부 시스템 붙이기

9.1 SQLite·PostgreSQL 연결 패턴

외부 시스템이라고 하면 보통 SaaS나 웹 API부터 떠올리기 쉽지만, 사실 데이터베이스가 가장 먼저 만나는 외부 세계입니다. 메모를 파일에만 저장하던 단계에서 벗어나면, 이제 서버는 상태를 오래 보존해야 하고, 동시에 검색도 빨라야 하며, 나중에는 여러 사용자까지 감당해야 합니다.

이때 가장 자주 비교하는 선택지가 SQLite와 PostgreSQL입니다.

SQLite는 혼자 일하는 개발자의 만능 수첩에 가깝습니다. 파일 하나로 끝나고, 로컬에서 빠르게 돌고, FTS5 같은 전문 검색 기능도 쓸 수 있습니다. 게다가 원본 파일이나 노트 폴더를 기준으로 다시 생성할 수 있는 캐시형 구조와도 아주 잘 맞습니다. 이런 패턴에서는 데이터베이스를 절대왕정의 원본 저장소가 아니라, 다시 만들 수 있는 인덱스이자 질의용 캐시로 보는 편이 훨씬 단순합니다.1

반면 PostgreSQL은 여러 사용자가 동시에 붙고, 원격 서버로 배포하고, 인증과 권한 분리가 필요해지는 순간부터 힘을 발휘합니다. 연결 풀을 두고, 마이그레이션을 관리하고, 운영 환경에 맞는 자격 증명과 접근 제어를 준비하는 식의 사고방식이 필요합니다.2

PKM-MCP에서는 이렇게 생각하면 편합니다.

  • 로컬 개발과 단일 사용자 실험은 SQLite로 시작한다.

  • 팀 공유, 원격 배포, 사용자별 데이터 격리는 PostgreSQL로 간다.

  • 둘 다 지원하되, 애플리케이션 코드는 저장소 계층으로 감춘다.

이 세 번째가 가장 중요합니다. 도구 함수 안에서 SQL을 직접 마구 호출하기 시작하면, 처음엔 빨라도 곧 서버 전체가 SQL 문자열 수집함이 됩니다. 나중에 SQLite에서 PostgreSQL로 옮길 때 가장 아픈 이유도 여기 있습니다.

그래서 PKM-MCP는 처음부터 저장소 계층을 둡니다.

from typing import Protocol, Any

class NoteRepository(Protocol):
    def create_note(self, title: str, content: str, tags: list[str]) -> dict: ...
    def update_note(self, note_id: str, title: str | None, content: str | None, tags: list[str] | None) -> dict: ...
    def delete_note(self, note_id: str) -> None: ...
    def search_notes(self, query: str, limit: int = 10) -> list[dict]: ...
    def upsert_external_note(self, note: dict[str, Any]) -> dict: ...

도구는 NoteRepository만 알고, 그 아래에서 SQLite 구현체든 PostgreSQL 구현체든 바꿔 끼우는 구조입니다. 이렇게 해두면 MCP 도구는 데이터베이스 종류보다 업무 의미에 집중할 수 있습니다.

예를 들어 도구는 이렇게 읽힙니다.

@mcp.tool()
def create_note(title: str, content: str, tags: list[str] = []) -> dict:
    '''새 노트를 만든다.'''
    return note_repo.create_note(title=title, content=content, tags=tags)

도구 입장에서는 뒤에 SQLite가 있든 PostgreSQL이 있든 상관이 없습니다. 이것이 구조의 힘입니다.

SQLite를 쓸 때의 추천 패턴

SQLite는 특히 PKM 같은 개인 지식 관리 프로젝트와 궁합이 좋습니다. 노트라는 데이터 자체가 아주 거대한 트랜잭션 시스템이 아니고, 읽기 비중이 높고, 로컬 우선으로 시작하기 좋기 때문입니다. 게다가 FTS5를 붙이면 제목과 본문을 꽤 빠르게 검색할 수 있습니다.1

다만 SQLite를 쓸 때도 규칙은 있어야 합니다. 특히 AI가 붙는 순간에는 더 그렇습니다. 읽기 전용 질의 도구와 쓰기 도구를 분리하고, 결과 개수 제한과 타임아웃 성격의 보호 장치를 생각해야 합니다. 데이터베이스를 AI에 연결할 때는 읽기 전용 계정, 결과 제한, 쿼리 시간 제한 같은 안전장치가 기본값이어야 한다는 점이 여러 실전 가이드에서 반복해서 강조됩니다.3

PKM-MCP의 초반 구조는 다음처럼 잡으면 무난합니다.

CREATE TABLE IF NOT EXISTS notes (
  id TEXT PRIMARY KEY,
  title TEXT NOT NULL,
  content TEXT NOT NULL,
  tags_json TEXT NOT NULL DEFAULT '[]',
  source TEXT NOT NULL DEFAULT 'local',
  external_id TEXT,
  source_url TEXT,
  metadata_json TEXT NOT NULL DEFAULT '{}',
  created_at TEXT NOT NULL,
  updated_at TEXT NOT NULL
);

CREATE UNIQUE INDEX IF NOT EXISTS idx_notes_source_external_id
ON notes(source, external_id)
WHERE external_id IS NOT NULL;

여기서 source와 external_id가 아주 중요합니다. 로컬에서 직접 쓴 노트는 source='local'일 수 있고, GitHub에서 가져온 이슈는 source='github-issue'가 됩니다. external_id는 외부 시스템의 고유 식별자입니다. 이 둘을 묶어 유니크 인덱스를 만들면, 같은 외부 데이터를 두 번 가져왔을 때 중복 생성 대신 덮어쓰기, 즉 upsert가 가능해집니다.

한마디로 말해, 이 인덱스는 가져오기를 동기화로 바꿔주는 문입니다.

PostgreSQL로 갈 때의 추천 패턴

PostgreSQL은 로컬 실험 단계에서는 조금 무겁게 느껴질 수 있습니다. 하지만 원격 서버가 되고 사용자가 늘어나면 이야기가 달라집니다. 연결 풀을 두고, 마이그레이션을 버전별로 관리하고, 환경 변수로 연결 정보를 주입하는 식의 운영 패턴이 필수가 됩니다.2

여기서 핵심은 세 가지입니다.

  • 애플리케이션 시작 시 연결 풀을 만든다.

  • 요청마다 연결을 새로 여닫지 않는다.

  • 스키마 변경은 버전된 마이그레이션으로 관리한다.

특히 MCP 서버는 도구 호출이 짧고 자주 일어나기 때문에, 매번 새 연결을 만드는 구조는 금방 비효율적이 됩니다. 반대로 PostgreSQL 풀을 한 번 잘 잡아두면, 이후의 도구는 대부분 같은 인터페이스 위에서 빠르게 움직입니다.2

그리고 운영 환경에서는 읽기와 쓰기를 권한 수준에서 분리하는 습관이 중요합니다. 예를 들어 PKM 자신의 데이터베이스에는 쓰기 권한이 있어야 하지만, 분석용 외부 데이터베이스를 붙일 때는 읽기 전용 계정으로 제한하는 쪽이 안전합니다. 쿼리 시간 제한과 결과 제한도 함께 두는 편이 좋습니다.3

PKM-MCP의 현실적인 선택

현 시점의 PKM-MCP는 이렇게 가져가면 좋습니다.

처음에는 SQLite로 개발합니다. 이유는 빠르고, 파일 하나면 되고, 메모라는 도메인과 잘 맞기 때문입니다. 다만 저장소 인터페이스는 처음부터 분리합니다. 그리고 3부에서 HTTP 전송, OAuth, 사용자별 격리로 넘어갈 때 PostgreSQL 구현체를 추가합니다.

즉, 데이터베이스를 바꾸는 게 아니라 저장소 어댑터를 교체하는 느낌으로 가야 합니다. 그러면 서버의 표정은 그대로인데, 뒤에 붙은 엔진만 교체됩니다.

9.2 기존 REST API를 MCP 도구로 감싸기

이제 데이터베이스 바깥으로 나가봅시다. 이미 회사 안에 잘 돌아가는 REST API가 있거나, 외부 SaaS가 HTTP API를 제공하는 경우가 많습니다. 이때 가장 위험한 유혹은 기존 API를 전부 다 MCP로 노출하자는 생각입니다.

하지만 좋은 MCP 래퍼는 API 문서를 그대로 복사한 것이 아닙니다. REST의 경로와 HTTP 메서드를 MCP 도구 이름으로 바꾸는 것만으로는, AI가 잘 쓰는 인터페이스가 되지 않습니다. MCP 도구는 라우트가 아니라 능력을 표현해야 합니다.5

예를 들어 이런 도구는 좋지 않습니다.

call_github_api(method, path, query, body, headers)

이건 사실상 AI에게 네가 알아서 HTTP 클라이언트가 되어라라고 말하는 것과 같습니다. 자유는 많지만, 사고도 많습니다.

반대로 이런 도구는 훨씬 좋습니다.

list_repo_issues(owner, repo, state, labels, limit)
create_issue_note(owner, repo, issue_number)
import_github_issues_as_notes(owner, repo, state, limit)

이쪽은 행동의 의미가 분명합니다. 언제 써야 하는지, 어떤 결과를 돌려주는지, 실패하면 왜 실패했는지 예측하기 쉽습니다.

REST API를 MCP로 감쌀 때는 다음 다섯 가지 원칙을 기억하면 됩니다.456

  • 도구 하나는 행동 하나만 맡긴다.

  • 입력은 경로, 쿼리, 바디를 합쳐 하나의 스키마로 평평하게 만든다.

  • 출력은 원본 API 응답을 그대로 넘기지 말고, 서버가 책임지고 안정된 형태로 다듬는다.

  • 인증 정보는 도구 인자에 넣지 말고 환경 변수나 서버 설정에서 주입한다.

  • 페이지네이션, 타임아웃, 레이트 리밋 같은 운영 문제를 처음부터 다룬다.

이 다섯 줄만 기억해도 래퍼의 품질이 크게 달라집니다.

얇게 감싸되, 너무 얇지는 않게

REST API 래핑에서 자주 나오는 조언이 있습니다. MCP는 두뇌가 아니라 어댑터여야 한다는 말입니다.4 이 말은 맞습니다. 하지만 그대로만 믿으면 반쪽짜리 해석이 됩니다.

정확히는, 비즈니스 로직을 MCP 안에 과하게 쌓아두지는 말되, AI가 쓰기 좋은 계약으로 재구성하는 책임은 MCP가 져야 합니다.

예를 들어 원본 API가 이런 응답을 준다고 해보겠습니다.

{
  "total_count": 1284,
  "incomplete_results": false,
  "items": [
    {
      "id": 123,
      "node_id": "...",
      "url": "...",
      "repository_url": "...",
      "labels_url": "...",
      "events_url": "...",
      "html_url": "...",
      "title": "Bug report",
      "body": "..."
    }
  ]
}

이걸 그대로 LLM에 던지면 보기엔 풍성하지만, 실제로는 소음이 많습니다. AI는 node_id보다 이슈 번호, 제목, 상태, 링크, 본문 요약이 더 필요합니다. 따라서 MCP 도구는 이렇게 응답을 깎아내는 편이 좋습니다.4

{
  "repo": "acme/pkm",
  "issues": [
    {
      "number": 123,
      "title": "Bug report",
      "state": "open",
      "url": "https://github.com/...",
      "body_preview": "..."
    }
  ],
  "has_more": true
}

정보를 버리는 것이 아니라, AI가 쓰기 좋은 모양으로 다시 담는 것입니다.

래퍼의 기본 뼈대

PKM-MCP에서는 도구 함수가 직접 httpx.get()를 난사하기보다, 작은 API 클라이언트를 하나 두는 편이 낫습니다.

import os
import httpx
from mcp.shared.exceptions import ToolError

class GitHubIssuesAPI:
    def __init__(self) -> None:
        token = os.getenv('GITHUB_TOKEN')
        if not token:
            raise RuntimeError('GITHUB_TOKEN 환경 변수가 필요합니다.')

        self.client = httpx.AsyncClient(
            base_url='https://api.github.com',
            headers={
                'Authorization': f'Bearer {token}',
                'Accept': 'application/vnd.github+json',
                'User-Agent': 'pkm-mcp'
            },
            timeout=20.0,
        )

    async def list_issues(
        self,
        owner: str,
        repo: str,
        state: str = 'open',
        labels: str | None = None,
        per_page: int = 30,
        page: int = 1,
    ) -> list[dict]:
        response = await self.client.get(
            f'/repos/{owner}/{repo}/issues',
            params={
                'state': state,
                'labels': labels,
                'per_page': per_page,
                'page': page,
            },
        )

        if response.status_code == 401:
            raise ToolError('GitHub 토큰이 유효하지 않습니다.')
        if response.status_code == 403:
            raise ToolError('GitHub API 요청 한도에 걸렸습니다. 잠시 후 다시 시도하세요.')
        if response.status_code == 404:
            raise ToolError('저장소를 찾을 수 없거나 접근 권한이 없습니다.')

        response.raise_for_status()
        return response.json()

이 구조의 장점은 분명합니다. 도구는 업무 의미를 담당하고, API 클라이언트는 HTTP 세부 사항을 담당합니다. 나중에 레이트 리밋 재시도나 로깅을 추가할 때도 한 곳만 고치면 됩니다.6

9.3 SaaS API 연동 — GitHub 예제

이제 실전으로 들어가겠습니다. 이번 장의 목표는 GitHub 이슈를 PKM 노트로 가져오는 도구를 추가하는 것입니다.

참고로 GitHub에는 이미 공식 MCP 서버와 hosted endpoint가 있습니다.7 그러니 엄밀히 말하면 GitHub를 MCP로 쓰기 위해 꼭 직접 서버를 만들 필요는 없습니다. 그런데도 PKM-MCP 안에 GitHub 가져오기 도구를 직접 넣는 이유는 분명합니다.

GitHub 이슈 자체가 목적이 아니라, 그것을 우리 시스템의 노트라는 내부 형식으로 흡수하는 것이 목적이기 때문입니다. 그래야 기존의 검색, RAG, 요약, 주간 리포트 같은 흐름에 바로 태울 수 있습니다.

여기서 먼저 결정해야 할 것은 세 가지입니다.

  • 외부 데이터를 노트로 복사할지, 링크만 참조할지, 양방향 동기화할지

  • 외부 시스템의 ID를 우리 시스템에서 어떻게 추적할지

  • 가져온 뒤에 어떤 메타데이터를 남길지

PKM-MCP에서는 우선 가져오기 + upsert 전략이 가장 현실적입니다. 외부 이슈를 내부 노트로 복사하되, source와 external_id를 보관해서 같은 이슈를 다시 가져오면 갱신되도록 만듭니다.

이 방식의 장점은 단순합니다. 기존 검색 인덱스에 바로 들어가고, 의미 검색에도 걸리고, 오프라인 상태에서도 노트로 남습니다. 그리고 나중에 sync_github_issue 같은 도구를 따로 만들어 동기화를 확장할 수도 있습니다.

이슈를 노트로 바꾸는 규칙

가져온 데이터를 노트로 저장할 때는 변환 규칙이 필요합니다. PKM-MCP에서는 다음처럼 매핑하면 이해하기 쉽습니다.

  • 노트 제목: [GH:owner/repo #번호] 이슈 제목

  • 노트 본문: 이슈 본문 + 원본 링크

  • 태그: github, repo:owner/repo, 라벨들

  • source: github-issue

  • external_id: owner/repo#번호

  • metadata: 작성자, 상태, 생성 시각, 수정 시각, URL

이 규칙의 좋은 점은 사람이 읽기 쉬우면서도, 나중에 기계가 다시 다루기 쉽다는 것입니다.

예를 들어 변환 함수는 이렇게 생길 수 있습니다.

def issue_to_note(issue: dict, owner: str, repo: str) -> dict:
    labels = [label['name'] for label in issue.get('labels', [])]
    title = f'[GH:{owner}/{repo} #{issue["number"]}] {issue["title"]}'
    body = issue.get('body') or '(본문 없음)'

    return {
        'title': title,
        'content': f'{body}nn---n원본 이슈: {issue["html_url"]}',
        'tags': ['github', f'repo:{owner}/{repo}', *labels],
        'source': 'github-issue',
        'external_id': f'{owner}/{repo}#{issue["number"]}',
        'source_url': issue['html_url'],
        'metadata': {
            'repo': f'{owner}/{repo}',
            'number': issue['number'],
            'state': issue['state'],
            'labels': labels,
            'author': (issue.get('user') or {}).get('login'),
            'created_at': issue.get('created_at'),
            'updated_at': issue.get('updated_at'),
        },
    }

여기서 포인트는 raw JSON 전체 저장이 아닙니다. 물론 필요하다면 원본도 저장할 수 있지만, 우선은 PKM 시스템에서 실제로 쓸 만한 정보 위주로 정규화하는 편이 좋습니다. 나중에 검색 결과나 요약 프롬프트에서 이 메타데이터가 훨씬 잘 쓰입니다.

만들기: PKM에 GitHub 이슈를 노트로 가져오는 도구 추가

이제 도구를 붙여봅시다. 이번 도구는 읽기 전용 API 호출을 한 뒤, 그 결과를 내부 노트로 쓰기 때문에 완전한 read-only는 아닙니다. 하지만 source + external_id 기준의 upsert를 쓰면 같은 입력에 대해 결과가 안정적이어서, 사실상 꽤 다루기 쉬운 idempotent 스타일의 도구가 됩니다.

1) 저장소에 upsert 메서드 추가하기

먼저 저장소 계층에 외부 노트 upsert를 만듭니다.

import json
from datetime import datetime, UTC
from uuid import uuid4

class SQLiteNoteRepository:
    def __init__(self, conn):
        self.conn = conn

    def upsert_external_note(self, note: dict) -> dict:
        now = datetime.now(UTC).isoformat()
        cur = self.conn.cursor()

        cur.execute(
            '''
            SELECT id FROM notes
            WHERE source = ? AND external_id = ?
            ''',
            (note['source'], note['external_id']),
        )
        row = cur.fetchone()

        if row:
            note_id = row[0]
            cur.execute(
                '''
                UPDATE notes
                SET title = ?,
                    content = ?,
                    tags_json = ?,
                    source_url = ?,
                    metadata_json = ?,
                    updated_at = ?
                WHERE id = ?
                ''',
                (
                    note['title'],
                    note['content'],
                    json.dumps(note['tags'], ensure_ascii=False),
                    note['source_url'],
                    json.dumps(note['metadata'], ensure_ascii=False),
                    now,
                    note_id,
                ),
            )
        else:
            note_id = str(uuid4())
            cur.execute(
                '''
                INSERT INTO notes (
                    id, title, content, tags_json,
                    source, external_id, source_url,
                    metadata_json, created_at, updated_at
                ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
                ''',
                (
                    note_id,
                    note['title'],
                    note['content'],
                    json.dumps(note['tags'], ensure_ascii=False),
                    note['source'],
                    note['external_id'],
                    note['source_url'],
                    json.dumps(note['metadata'], ensure_ascii=False),
                    now,
                    now,
                ),
            )

        self.conn.commit()
        return {'id': note_id, 'title': note['title'], 'source': note['source']}

이제 외부 데이터는 create_note()가 아니라 upsert_external_note()로 들어옵니다. 이 차이를 나눠두면 로컬 메모 작성과 외부 동기화가 헷갈리지 않습니다.

2) GitHub 도구 추가하기

다음으로 MCP 도구를 붙입니다.

from typing import Literal
from mcp.server.fastmcp import FastMCP
from mcp.shared.exceptions import ToolError

mcp = FastMCP('PKM-MCP')
github_api = GitHubIssuesAPI()

@mcp.tool()
async def import_github_issues_as_notes(
    owner: str,
    repo: str,
    state: Literal['open', 'closed', 'all'] = 'open',
    label: str | None = None,
    limit: int = 20,
) -> dict:
    '''
    GitHub 저장소의 이슈를 가져와 PKM 노트로 저장한다.

    언제 쓰나:
    - 특정 저장소의 이슈를 개인 지식 베이스에 흡수하고 싶을 때
    - 이후 search_notes, semantic_search_notes, 요약 프롬프트에서 재사용하고 싶을 때

    규칙:
    - 같은 이슈를 다시 가져오면 source + external_id 기준으로 갱신한다.
    - limit는 1~100 사이여야 한다.
    '''
    if not owner.strip():
        raise ToolError('owner는 비어 있을 수 없습니다.')
    if not repo.strip():
        raise ToolError('repo는 비어 있을 수 없습니다.')
    if limit < 1 or limit > 100:
        raise ToolError('limit는 1 이상 100 이하여야 합니다.')

    imported: list[dict] = []
    page = 1
    remaining = limit

    while remaining > 0:
        batch = await github_api.list_issues(
            owner=owner,
            repo=repo,
            state=state,
            labels=label,
            per_page=min(remaining, 100),
            page=page,
        )

        if not batch:
            break

        for issue in batch:
            if 'pull_request' in issue:
                continue

            note = issue_to_note(issue=issue, owner=owner, repo=repo)
            saved = note_repo.upsert_external_note(note)
            imported.append({
                'note_id': saved['id'],
                'issue_number': issue['number'],
                'title': issue['title'],
                'state': issue['state'],
                'url': issue['html_url'],
            })
            remaining -= 1

            if remaining == 0:
                break

        page += 1

    return {
        'repo': f'{owner}/{repo}',
        'imported_count': len(imported),
        'items': imported[:10],
        'message': '가져온 이슈는 PKM 노트로 저장되며, 동일 이슈 재가져오기는 갱신으로 처리됩니다.',
    }

이 도구의 장점은 아주 명확합니다. AI가 GitHub API 구조를 자세히 몰라도 됩니다. 그냥 어느 저장소의 어떤 이슈를 노트로 가져와 달라고 요청하면 됩니다. 그리고 결과는 곧바로 PKM 내부 검색 대상이 됩니다.

3) 직접 써보기

이제 Claude Desktop이나 Inspector에서 이런 식으로 요청할 수 있습니다.

  • owner=fastapi, repo=fastapi, state=open, limit=5로 GitHub 이슈를 노트로 가져와 줘.

  • owner=langchain-ai, repo=langchain, label=bug, state=open, limit=10으로 가져와 줘.

  • 방금 가져온 GitHub 이슈 노트들을 요약해서 이번 주에 볼 만한 항목만 추려줘.

마지막 요청이 재미있습니다. 가져오기 도구는 단지 복사만 하는 것처럼 보여도, 그다음부터는 기존의 search_notes, semantic_search_notes, 이번 주 메모 요약 프롬프트와 바로 합쳐집니다. 외부 데이터가 내부 지식으로 변환되는 순간입니다.

이 장에서 꼭 기억할 설계 포인트

이번 장은 코드를 붙이는 것보다 설계를 익히는 장입니다. 특히 아래 세 가지는 앞으로 계속 반복해서 나옵니다.

첫째, 외부 시스템은 직접 노출하지 말고 내부 도메인 언어로 번역해야 합니다. GitHub 이슈를 그대로 노출하는 것보다, PKM 노트라는 형식으로 흡수하는 편이 이후 활용도가 훨씬 높습니다.

둘째, 동기화에는 고유 키가 필요합니다. source + external_id가 없으면 외부 데이터 통합은 금방 중복 노트 파티가 됩니다. 처음엔 사소해 보여도, 나중에는 가장 큰 차이를 만듭니다.

셋째, AI에게는 자유보다 경계가 중요합니다. 읽기 전용 계정, 결과 제한, 작은 응답, 안정된 스키마, 짧고 분명한 에러 메시지 같은 요소가 결국 도구 품질을 만듭니다.346

마치며

이제 PKM-MCP는 제법 그럴듯한 모습을 갖추었습니다. 로컬 메모를 다루는 CRUD 서버이면서, 외부 시스템의 데이터를 노트로 흡수할 수 있는 통합 서버가 되었습니다.

그리고 아주 중요한 변화가 하나 생겼습니다. 지금까지는 PKM-MCP가 자기 안의 데이터만 다루는 서버였다면, 이제부터는 바깥세상의 정보를 안으로 끌어와 자신의 기억으로 만드는 서버가 되었다는 점입니다.

다음 장에서는 이 서버를 더 넓은 세상으로 내보냅니다. 지금까지는 주로 stdio로 붙어 있던 서버를 Streamable HTTP로 옮기면서, 로컬 장난감이 아니라 원격 서비스로 운영하는 사고방식이 시작됩니다.

참고

1The MCP Pattern: SQLite as the AI-Queryable Cache

2Async-IO/pierre_mcp_server - Database Setup and Migration

3How to use MCP to query databases from AI assistants

4Wrap Existing APIs as MCP Tools (Simple Guide)

5How to map an existing API into MCP tool definitions

6How to build an MCP server that wraps an external API

7A practical guide on how to use the GitHub MCP server

책 목록

만들면서 배우는 MCP: PKM-MCP로 끝내는 설계·구현·운영·배포

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기