가상 식당 예약 장부(reservations.csv)를 클로드가 조회·추가하게 해 주는 MCP 서버를, 같은 요청으로 플러그인 없이 2번·있이 2번 만들게 한 기록입니다. Claude Code · Opus 5.5 · 생각 깊이 중간. 채점은 만든 서버를 실제로 띄워 MCP 클라이언트로 8가지를 확인했습니다. 이름·전화번호는 모두 가상입니다.
요청: 우리 가게(작은 식당) 예약 장부 reservations.csv를 클로드가 직접 조회하고 새 예약을 넣을 수 있게 MCP 서버를 만들어 줘. 내 맥에서 나만 쓰고, 할 일은 '날짜나 이름으로 예약 찾기'와 '새 예약 추가' 두 가지야. 화면이나 로그인은 필요 없어. 파이썬으로 만들고, 파이썬 환경은 ../.venv 에 mcp와 fastmcp 패키지가 이미 설치돼 있어(새로 설치하지 마). 다 만들면 이 폴더에서 서버를 띄우는 명령을 run.txt에 한 줄로 적어 줘.
| 조건 | 채점 | 부른 스킬 | 사용량(API 단가) |
|---|---|---|---|
| 플러그인 없이 1회차 | 6/8 | 해당 없음 | 0.50달러 |
| 플러그인 없이 2회차 | 6/8 | 해당 없음 | 0.51달러 |
| 플러그인 있이 1회차 | 7/8 | mcp-server-dev:build-mcp-server | 0.58달러 |
| 플러그인 있이 2회차 | 7/8 | 부르지 않음 | 0.51달러 |
요청: 10월 21일 예약 보여 주고, 23일 저녁 7시에 정하늘 님 2명(010-7777-8888) T4로 예약 넣어 줘.
→ find_reservations {"date": "2026-10-21"}
→ add_reservation {"date": "2026-10-23", "time": "19:00", "name": "정하늘", "phone": "010-7777-8888", "party": 2, "table": "T4"}10월 21일 예약은 2건입니다. | 번호 | 시간 | 예약자 | 연락처 | 인원 | 테이블 | 메모 | |---|---|---|---|---|---|---| | R-0003 | 12:00 | 박지훈 | 010-9876-5432 | 6명 | T5 | 생일, 케이크 반입 | | R-0004 | 18:30 | 최유진 | 010-5555-1111 | 3명 | T2 | 유아 의자 1개 | 말씀하신 예약은 **R-0006**으로 등록했습니다. 10월 23일 19:00, 정하늘 님 2명, 연락처 010-7777-8888, 테이블 T4입니다.
id,date,time,name,phone,party,table,memo R-0001,2026-10-20,18:00,김민수,010-1234-5678,4,T3,창가 자리 원함 R-0002,2026-10-20,19:30,이서연,010-2222-3333,2,T1, R-0003,2026-10-21,12:00,박지훈,010-9876-5432,6,T5,"생일, 케이크 반입" R-0004,2026-10-21,18:30,최유진,010-5555-1111,3,T2,유아 의자 1개 R-0005,2026-10-22,19:00,김민수,010-1234-5678,2,T1,
"""가게 예약 장부(reservations.csv)를 조회하고 새 예약을 추가하는 MCP 서버."""
import csv
import fcntl
import io
import re
from datetime import date as Date
from pathlib import Path
from fastmcp import FastMCP
CSV_PATH = Path(__file__).resolve().parent / "reservations.csv"
FIELDS = ["id", "date", "time", "name", "phone", "party", "table", "memo"]
mcp = FastMCP(
"reservations",
instructions=(
"작은 식당의 예약 장부입니다. find_reservations로 날짜나 이름으로 예약을 찾고, "
"add_reservation으로 새 예약을 추가합니다. 날짜는 YYYY-MM-DD, 시간은 HH:MM(24시간) 형식입니다."
),
)
def _check_date(value: str) -> str:
try:
return Date.fromisoformat(value.strip()).isoformat()
except ValueError:
raise ValueError(f"날짜 형식이 올바르지 않습니다: {value!r} (예: 2026-10-20)")
def _check_time(value: str) -> str:
m = re.fullmatch(r"(\d{1,2}):(\d{2})", value.strip())
if not m or int(m.group(1)) > 23 or int(m.group(2)) > 59:
raise ValueError(f"시간 형식이 올바르지 않습니다: {value!r} (예: 18:30)")
return f"{int(m.group(1)):02d}:{m.group(2)}"
@mcp.tool(annotations={"readOnlyHint": True})
def find_reservations(date: str | None = None, name: str | None = None) -> list[dict]:
"""날짜나 이름으로 예약을 찾습니다.
Args:
date: 찾을 날짜 (YYYY-MM-DD). 생략하면 날짜로 거르지 않습니다.
name: 예약자 이름. 일부만 입력해도 찾습니다(예: '김'). 생략하면 이름으로 거르지 않습니다.
둘 다 주면 두 조건을 모두 만족하는 예약만, 둘 다 생략하면 전체 예약을 날짜·시간순으로 돌려줍니다.
"""
target_date = _check_date(date) if date else None
target_name = name.strip() if name else None
with open(CSV_PATH, newline="", encoding="utf-8") as f:
fcntl.flock(f, fcntl.LOCK_SH)
rows = list(csv.DictReader(f))
results = [
r for r in rows
if (target_date is None or r["date"] == target_date)
and (not target_name or target_name in r["name"])
]
return sorted(results, key=lambda r: (r["date"], r["time"]))
@mcp.tool
def add_reservation(
date: str,
time: str,
name: str,
phone: str,
party: int,
table: str = "",
memo: str = "",
) -> dict:
"""새 예약을 장부에 추가하고, 추가된 예약(새 예약번호 포함)을 돌려줍니다.
Args:
date: 예약 날짜 (YYYY-MM-DD)
time: 예약 시간 (HH:MM, 24시간)
name: 예약자 이름
phone: 연락처 (예: 010-1234-5678)
party: 인원 수
table: 테이블 번호 (예: T3). 아직 안 정했으면 비워 둡니다.
memo: 요청 사항 등 메모
같은 날짜·시간에 같은 테이블이 이미 예약돼 있으면 추가하지 않고 오류를 냅니다.
"""
date = _check_date(date)
time = _check_time(time)
name, phone, table, memo = name.strip(), phone.strip(), table.strip(), memo.strip()
if not name:
raise ValueError("예약자 이름이 필요합니다.")
if not phone:
raise ValueError("연락처가 필요합니다.")
if party < 1:
raise ValueError("인원 수는 1명 이상이어야 합니다.")
with open(CSV_PATH, "r+", newline="", encoding="utf-8") as f:
fcntl.flock(f, fcntl.LOCK_EX)
text = f.read()
rows = list(csv.DictReader(io.StringIO(text)))
if table:
for r in rows:
if r["date"] == date and r["time"] == time and r["table"] == table:
raise ValueError(
f"{date} {time} {table} 테이블은 이미 예약돼 있습니다 "
f"({r['id']}, {r['name']}). 다른 테이블이나 시간을 지정해 주세요."
)
numbers = [int(m.group(1)) for r in rows if (m := re.fullmatch(r"R-(\d+)", r["id"]))]
new = {
"id": f"R-{max(numbers, default=0) + 1:04d}",
"date": date,
"time": time,
"name": name,
"phone": phone,
"party": str(party),
"table": table,
"memo": memo,
}
# 파일이 줄바꿈 없이 끝나 있으면 새 줄이 앞 줄에 붙지 않게 보정
if text and not text.endswith(("\n", "\r")):
f.write("\n")
csv.DictWriter(f, fieldnames=FIELDS, lineterminator="\n").writerow(new)
return new
if __name__ == "__main__":
mcp.run()
예약 장부용 MCP 서버를 만들었습니다. 장부 사본으로 실제 MCP 연결을 띄워 테스트했고 모든 경우가 의도대로 동작했습니다. 원본 `reservations.csv`는 건드리지 않았습니다. **만든 파일** - `server.py`: MCP 서버 본체입니다. 이미 깔려 있는 fastmcp 4.1.1로 만들었고, 새로 설치한 것은 없습니다. - `run.txt`: 이 폴더에서 서버를 띄우는 명령입니다. ``` ../.venv/bin/python server.py ``` **클로드가 쓸 수 있는 기능 2가지** 1. **`find_reservations`(예약 찾기)**: 날짜(`2026-10-20` 형식)나 이름으로 찾습니다. 이름은 "김"처럼 일부만 넣어도 되고, 날짜와 이름을 같이 주면 둘 다 맞는 예약만 나옵니다. 결과는 날짜·시간 순으로 정렬됩니다. 2. **`add_reservation`(새 예약 추가)**: 날짜·시간·이름·연락처·인원은 꼭 넣어야 하고, 테이블과 메모는 비워 둬도 됩니다. - 예약번호는 `R-0006`처럼 자동으로 다음 번호가 붙습니다. - `8:30`처럼 넣은 시간은 `08:30`으로 맞춰 저장됩니다. - 날짜·시간이 틀린 형식이거나 인원이 0명이면 추가하지 않고 이유를 알려 줍니다. - 쉼표가 들어간 메모도 장부 형식이 깨지지 않게 저장됩니다. **제가 임의로 넣은 규칙 하나**: 같은 날짜·시간에 같은 테이블이 이미 잡혀 있으면 추가하지 않고, 누가 잡고 있는지 알려 줍니다. 원하지 않으시면 빼 드리겠습니다. **클로드에 연결하기**: 이 단계는 요청 범위 밖이라 실행하지 않았습니다. Claude Code에서 쓰시려면 아래 명령을 한 번 실행하면 됩니다. 연결 설정에서는 상대 경로가 안 맞을 수 있어 절대 경로로 적었습니다. ``` claude mcp add reservations -- /Users/euiseong/aiskillwiki-lab/claude-mcp-server-dev/.venv/bin/python /Users/euiseong/aiskillwiki-lab/claude-mcp-server-dev/none1/server.py ```
"""식당 예약 장부(reservations.csv) MCP 서버.
도구 두 개만 제공한다:
- find_reservations: 날짜 및/또는 이름으로 예약 찾기
- add_reservation: 새 예약 추가
stdio로 동작하며, 이 파일과 같은 폴더의 reservations.csv를 읽고 쓴다.
"""
import csv
import fcntl
import re
from datetime import datetime
from pathlib import Path
from fastmcp import FastMCP
CSV_PATH = Path(__file__).resolve().parent / "reservations.csv"
FIELDS = ["id", "date", "time", "name", "phone", "party", "table", "memo"]
mcp = FastMCP(
"restaurant-reservations",
instructions=(
"작은 식당의 예약 장부(reservations.csv)를 다룬다. "
"날짜는 YYYY-MM-DD, 시간은 HH:MM(24시간) 형식을 쓴다. "
"예약을 추가하기 전에 같은 날짜의 기존 예약을 먼저 조회해 겹치는 테이블이 없는지 확인하면 좋다."
),
)
def _read_rows() -> list[dict]:
if not CSV_PATH.exists():
return []
with CSV_PATH.open(newline="", encoding="utf-8-sig") as f:
return [dict(row) for row in csv.DictReader(f)]
def _normalize_date(value: str) -> str:
value = value.strip()
try:
return datetime.strptime(value, "%Y-%m-%d").strftime("%Y-%m-%d")
except ValueError:
raise ValueError(f"날짜 형식이 올바르지 않습니다: {value!r} (예: 2026-10-20)")
def _normalize_time(value: str) -> str:
value = value.strip()
try:
return datetime.strptime(value, "%H:%M").strftime("%H:%M")
except ValueError:
raise ValueError(f"시간 형식이 올바르지 않습니다: {value!r} (예: 18:30)")
def _next_id(rows: list[dict]) -> str:
nums = [int(m.group(1)) for r in rows if (m := re.fullmatch(r"R-(\d+)", r.get("id", "")))]
return f"R-{(max(nums, default=0) + 1):04d}"
@mcp.tool(annotations={"readOnlyHint": True})
def find_reservations(date: str | None = None, name: str | None = None) -> list[dict]:
"""날짜 및/또는 손님 이름으로 예약을 찾는다.
Args:
date: 예약 날짜 (YYYY-MM-DD). 생략 가능.
name: 손님 이름. 일부만 입력해도 찾는다(예: "민수"). 생략 가능.
date와 name 중 최소 하나는 줘야 하며, 둘 다 주면 두 조건을 모두 만족하는 예약만 돌려준다.
결과는 날짜·시간 순으로 정렬된다.
"""
if not (date and date.strip()) and not (name and name.strip()):
raise ValueError("date 또는 name 중 하나 이상을 입력하세요.")
rows = _read_rows()
if date and date.strip():
d = _normalize_date(date)
rows = [r for r in rows if r["date"] == d]
if name and name.strip():
q = name.strip().replace(" ", "")
rows = [r for r in rows if q in r["name"].replace(" ", "")]
return sorted(rows, key=lambda r: (r["date"], r["time"]))
@mcp.tool
def add_reservation(
date: str,
time: str,
name: str,
phone: str,
party: int,
table: str = "",
memo: str = "",
) -> dict:
"""새 예약을 장부에 추가하고, 저장된 예약(새 id 포함)을 돌려준다.
Args:
date: 예약 날짜 (YYYY-MM-DD)
time: 예약 시간 (HH:MM, 24시간)
name: 손님 이름
phone: 연락처 (예: 010-1234-5678)
party: 인원 수 (1 이상)
table: 테이블 번호 (예: T3). 생략 가능.
memo: 요청 사항 메모. 생략 가능.
같은 날짜·시간에 같은 테이블이 이미 예약돼 있으면 추가하지 않고 오류를 낸다.
"""
new = {
"date": _normalize_date(date),
"time": _normalize_time(time),
"name": name.strip(),
"phone": phone.strip(),
"party": party,
"table": table.strip().upper(),
"memo": memo.strip(),
}
if not new["name"]:
raise ValueError("이름을 입력하세요.")
if not new["phone"]:
raise ValueError("연락처를 입력하세요.")
if party < 1:
raise ValueError("인원 수는 1 이상이어야 합니다.")
# 읽기→검사→추가를 잠금 안에서 처리해 동시에 두 건이 들어와도 id가 겹치지 않게 한다.
with CSV_PATH.open("a+", newline="", encoding="utf-8") as f:
fcntl.flock(f, fcntl.LOCK_EX)
try:
rows = _read_rows()
if new["table"]:
for r in rows:
if (r["date"], r["time"], r["table"].upper()) == (new["date"], new["time"], new["table"]):
raise ValueError(
f"{new['date']} {new['time']} {new['table']} 테이블은 이미 예약돼 있습니다 "
f"({r['id']}, {r['name']})."
)
record = {"id": _next_id(rows), **new}
f.seek(0, 2)
if f.tell() == 0:
csv.writer(f, lineterminator="\n").writerow(FIELDS)
else:
# 마지막 줄이 줄바꿈 없이 끝나면 새 행이 붙어버리지 않게 보정
with CSV_PATH.open("rb") as rb:
rb.seek(-1, 2)
if rb.read(1) not in (b"\n", b"\r"):
f.write("\n")
csv.DictWriter(f, fieldnames=FIELDS, lineterminator="\n").writerow(record)
f.flush()
finally:
fcntl.flock(f, fcntl.LOCK_UN)
return record
if __name__ == "__main__":
mcp.run(show_banner=False)
예약 장부용 MCP 서버를 만들었습니다. 장부 사본에 실제 MCP 연결을 붙여서 조회와 추가가 모두 잘 되는 것까지 확인했고, 원본 `reservations.csv`는 건드리지 않았습니다. 패키지도 새로 설치하지 않았습니다. **만든 파일** - `server.py`: 서버 본체입니다. 같은 폴더의 `reservations.csv`를 읽고 씁니다. - `run.txt`: 서버를 띄우는 명령 한 줄입니다. ``` ../.venv/bin/python server.py ``` **할 수 있는 일 두 가지** 1. **예약 찾기** (`find_reservations`) - 날짜(예: 2026-10-20)나 이름으로 찾습니다. 둘 다 주면 두 조건에 모두 맞는 예약만 나옵니다. - 이름은 일부만 넣어도 됩니다. "민수"로 찾으면 김민수 예약 2건이 나왔습니다. - 결과는 날짜·시간 순으로 정렬됩니다. - 날짜와 이름을 둘 다 비우면 장부 전체를 보여주지 않고 둘 중 하나를 달라는 오류를 냅니다. 2. **새 예약 추가** (`add_reservation`) - 날짜, 시간, 이름, 전화번호, 인원을 받고, 테이블 번호와 메모는 넣어도 되고 빼도 됩니다. - 예약번호는 다음 번호로 자동으로 붙습니다. 지금 장부라면 R-0006이 됩니다. - 날짜·시간 형식이 틀리거나, 인원이 0명 이하거나, 같은 날 같은 시간에 이미 예약된 테이블이면 추가하지 않고 이유를 알려줍니다. - 메모에 쉼표가 있어도 칸이 깨지지 않게 저장됩니다. **Claude에 연결하는 방법** Claude는 이 폴더 밖에서 서버를 실행하기 때문에, 연결할 때는 `run.txt`의 상대 경로 대신 아래처럼 절대 경로를 써야 합니다. ``` claude mcp add reservations -- /Users/euiseong/aiskillwiki-lab/claude-mcp-server-dev/.venv/bin/python /Users/euiseong/aiskillwiki-lab/claude-mcp-server-dev/none2/server.py ```
"""가게 예약 장부(reservations.csv)를 조회·추가하는 로컬 MCP 서버 (stdio)."""
import csv
import fcntl
import io
import re
from datetime import datetime
from pathlib import Path
from typing import Annotated
from fastmcp import FastMCP
from pydantic import Field
CSV_PATH = Path(__file__).resolve().parent / "reservations.csv"
FIELDS = ["id", "date", "time", "name", "phone", "party", "table", "memo"]
mcp = FastMCP(
"reservations",
instructions=(
"작은 식당의 예약 장부(reservations.csv)를 다룹니다. "
"예약 확인은 find_reservations, 새 예약은 add_reservation을 쓰세요. "
"날짜는 YYYY-MM-DD, 시간은 HH:MM(24시간) 형식입니다."
),
)
def _read_rows(f) -> list[dict]:
f.seek(0)
return list(csv.DictReader(f))
def _check_date(value: str) -> str:
try:
return datetime.strptime(value.strip(), "%Y-%m-%d").strftime("%Y-%m-%d")
except ValueError:
raise ValueError(f"날짜 형식이 잘못됐습니다: {value!r} (예: 2026-10-20)")
def _check_time(value: str) -> str:
try:
return datetime.strptime(value.strip(), "%H:%M").strftime("%H:%M")
except ValueError:
raise ValueError(f"시간 형식이 잘못됐습니다: {value!r} (예: 18:30)")
@mcp.tool(annotations={"readOnlyHint": True})
def find_reservations(
date: Annotated[str | None, Field(description="찾을 날짜 YYYY-MM-DD. 비우면 날짜 무관")] = None,
name: Annotated[str | None, Field(description="예약자 이름(일부만 넣어도 됨). 비우면 이름 무관")] = None,
) -> list[dict]:
"""날짜나 예약자 이름으로 예약을 찾습니다. 둘 다 주면 둘 다 맞는 예약만 돌려줍니다.
결과는 날짜·시간 순이며, 없으면 빈 목록입니다."""
if not date and not name:
raise ValueError("date 또는 name 중 하나는 꼭 넣어 주세요.")
date = _check_date(date) if date else None
name = name.strip() if name else None
with open(CSV_PATH, newline="", encoding="utf-8") as f:
fcntl.flock(f, fcntl.LOCK_SH)
rows = _read_rows(f)
hits = [
r for r in rows
if (date is None or r["date"] == date)
and (name is None or name in r["name"])
]
return sorted(hits, key=lambda r: (r["date"], r["time"]))
@mcp.tool(annotations={"readOnlyHint": False, "destructiveHint": False, "idempotentHint": False})
def add_reservation(
date: Annotated[str, Field(description="예약 날짜 YYYY-MM-DD")],
time: Annotated[str, Field(description="예약 시간 HH:MM (24시간)")],
name: Annotated[str, Field(description="예약자 이름")],
phone: Annotated[str, Field(description="연락처, 예: 010-1234-5678")],
party: Annotated[int, Field(ge=1, description="인원 수")],
table: Annotated[str, Field(description="테이블 번호, 예: T3. 미정이면 빈 문자열")] = "",
memo: Annotated[str, Field(description="요청사항 메모")] = "",
) -> dict:
"""장부에 새 예약을 추가하고, 새 예약 번호(R-0006 등)가 붙은 예약 내용을 돌려줍니다.
같은 날짜·시간·테이블에 이미 예약이 있으면 추가하지 않고 오류를 냅니다."""
date = _check_date(date)
time = _check_time(time)
name, phone, table, memo = name.strip(), phone.strip(), table.strip(), memo.strip()
if not name:
raise ValueError("예약자 이름이 비어 있습니다.")
if not phone:
raise ValueError("연락처가 비어 있습니다.")
with open(CSV_PATH, "r+", newline="", encoding="utf-8") as f:
fcntl.flock(f, fcntl.LOCK_EX)
text = f.read()
rows = list(csv.DictReader(io.StringIO(text)))
if table:
for r in rows:
if r["date"] == date and r["time"] == time and r["table"] == table:
raise ValueError(
f"{date} {time} {table}에 이미 예약이 있습니다 ({r['id']}, {r['name']})."
)
nums = [int(m.group(1)) for r in rows if (m := re.fullmatch(r"R-(\d+)", r["id"]))]
new = {
"id": f"R-{max(nums, default=0) + 1:04d}",
"date": date, "time": time, "name": name, "phone": phone,
"party": str(party), "table": table, "memo": memo,
}
# 파일 끝에 줄바꿈이 없으면 붙여서 기존 마지막 줄과 섞이지 않게 함
f.seek(0, 2)
if text and not text.endswith("\n"):
f.write("\n")
csv.writer(f, lineterminator="\n").writerow([new[k] for k in FIELDS])
return new
if __name__ == "__main__":
mcp.run(show_banner=False)
예약 장부용 MCP 서버를 만들었습니다. 장부 복사본에 실제 MCP 클라이언트로 연결해서 조회, 추가, 오류 처리를 모두 테스트했고 전부 통과했습니다. 원래 `reservations.csv`는 건드리지 않았습니다. 패키지도 새로 설치하지 않았고 `../.venv`에 있던 것만 썼습니다.
**만든 파일**
- `server.py`: 서버 본체입니다. 기능은 두 가지입니다.
- **`find_reservations`**: 날짜나 이름으로 예약을 찾습니다. 이름은 일부만 넣어도 되고("민수"로 "김민수" 찾기), 둘 다 넣으면 둘 다 맞는 예약만 나옵니다. 결과는 날짜·시간 순입니다. 날짜와 이름을 둘 다 비우면 오류가 납니다.
- **`add_reservation`**: 날짜, 시간, 이름, 연락처, 인원을 받아 장부 맨 아래에 한 줄을 추가합니다. 테이블과 메모는 넣지 않아도 됩니다. 예약 번호는 마지막 번호 다음으로 자동으로 붙습니다(지금 장부라면 R-0006). 쉼표가 들어간 메모도 CSV에 제대로 저장됩니다.
- `run.txt`: 이 폴더에서 서버를 띄우는 명령입니다.
```
../.venv/bin/python server.py
```
**요청에 없던 것 중 제가 정해서 넣은 것**
- 날짜·시간·테이블이 모두 같은 예약이 이미 있으면 추가하지 않고 기존 예약 번호와 이름을 알려줍니다. 테이블을 비워 두면 이 확인은 하지 않습니다.
- 날짜는 2026-10-20, 시간은 18:30 형식만 받습니다. 인원은 1명 이상이어야 합니다.
- 장부를 읽고 쓰는 동안 파일을 잠가서, 추가가 거의 동시에 두 번 들어와도 내용이 섞이지 않게 했습니다.
**Claude에 연결하기 (아직 안 했습니다)**
Claude는 서버를 직접 띄우기 때문에 등록할 때는 절대경로를 써야 합니다. Claude Code라면 아래 한 줄로 등록할 수 있습니다.
```
claude mcp add reservations -- /Users/euiseong/aiskillwiki-lab/claude-mcp-server-dev/.venv/bin/python /Users/euiseong/aiskillwiki-lab/claude-mcp-server-dev/plug1/server.py
```
등록하면 "10월 21일 예약 보여줘", "23일 저녁 6시 정하늘 5명 T4로 예약 넣어줘" 같은 요청을 Claude가 바로 처리할 수 있습니다."""가게 예약 장부(reservations.csv)를 조회·추가하는 로컬 MCP 서버 (stdio)."""
import csv
import fcntl
import re
from datetime import date as Date, time as Time
from pathlib import Path
from typing import Annotated
from fastmcp import FastMCP
from fastmcp.exceptions import ToolError
from pydantic import Field
CSV_PATH = Path(__file__).resolve().parent / "reservations.csv"
FIELDS = ["id", "date", "time", "name", "phone", "party", "table", "memo"]
mcp = FastMCP(
"restaurant-reservations",
instructions=(
"작은 식당의 예약 장부. 날짜(YYYY-MM-DD)나 손님 이름으로 예약을 찾고, "
"새 예약을 추가할 수 있다. 추가 전에 같은 날 예약을 먼저 조회해 테이블 충돌을 확인하면 좋다."
),
)
def _read_rows() -> list[dict]:
with CSV_PATH.open(newline="", encoding="utf-8") as f:
return list(csv.DictReader(f))
def _next_id(rows: list[dict]) -> str:
nums = [int(m.group(1)) for r in rows if (m := re.fullmatch(r"R-(\d+)", r["id"]))]
return f"R-{max(nums, default=0) + 1:04d}"
@mcp.tool(annotations={"readOnlyHint": True})
def find_reservations(
date: Annotated[str | None, Field(description="예약 날짜, YYYY-MM-DD 형식 (예: 2026-10-20)")] = None,
name: Annotated[str | None, Field(description="손님 이름 (일부만 입력해도 됨, 예: '민수')")] = None,
) -> dict:
"""날짜나 이름(또는 둘 다)으로 예약을 찾는다. 결과는 날짜·시간 순으로 정렬된다."""
if not date and not name:
raise ToolError("date 또는 name 중 하나 이상을 지정하세요.")
if date:
try:
Date.fromisoformat(date)
except ValueError:
raise ToolError(f"날짜 형식이 잘못됐습니다: {date!r} (YYYY-MM-DD 형식으로 입력)")
rows = _read_rows()
if date:
rows = [r for r in rows if r["date"] == date]
if name:
key = name.strip()
rows = [r for r in rows if key in r["name"]]
rows.sort(key=lambda r: (r["date"], r["time"]))
return {"count": len(rows), "reservations": rows}
@mcp.tool(annotations={"readOnlyHint": False, "destructiveHint": False, "idempotentHint": False})
def add_reservation(
date: Annotated[str, Field(description="예약 날짜, YYYY-MM-DD 형식")],
time: Annotated[str, Field(description="예약 시간, 24시간제 HH:MM 형식 (예: 18:30)")],
name: Annotated[str, Field(description="손님 이름")],
phone: Annotated[str, Field(description="연락처 (예: 010-1234-5678)")],
party: Annotated[int, Field(description="인원 수", ge=1)],
table: Annotated[str, Field(description="테이블 번호 (예: T3). 모르면 빈 문자열")] = "",
memo: Annotated[str, Field(description="요청사항 메모")] = "",
) -> dict:
"""새 예약을 장부에 추가한다. 같은 날짜·시간·테이블에 이미 예약이 있으면 거절한다. 추가된 예약(새 id 포함)을 돌려준다."""
try:
date = Date.fromisoformat(date).isoformat()
except ValueError:
raise ToolError(f"날짜 형식이 잘못됐습니다: {date!r} (YYYY-MM-DD 형식으로 입력)")
if not re.fullmatch(r"\d{1,2}:\d{2}", time.strip()):
raise ToolError(f"시간 형식이 잘못됐습니다: {time!r} (HH:MM 형식으로 입력)")
try:
time = Time.fromisoformat(time.strip().zfill(5)).strftime("%H:%M")
except ValueError:
raise ToolError(f"존재하지 않는 시간입니다: {time!r}")
name, phone, table = name.strip(), phone.strip(), table.strip().upper()
memo = " ".join(memo.split()) # 줄바꿈 제거
if not name:
raise ToolError("이름은 비워둘 수 없습니다.")
# 읽기→중복 확인→추가를 한 번의 파일 잠금 안에서 처리
with CSV_PATH.open("r+", newline="", encoding="utf-8") as f:
fcntl.flock(f, fcntl.LOCK_EX)
rows = list(csv.DictReader(f))
if table:
clash = next(
(r for r in rows if r["date"] == date and r["time"] == time and r["table"].upper() == table),
None,
)
if clash:
raise ToolError(
f"{date} {time} {table}에는 이미 예약이 있습니다 "
f"({clash['id']}, {clash['name']}). 다른 테이블이나 시간을 선택하세요."
)
new = {
"id": _next_id(rows), "date": date, "time": time, "name": name,
"phone": phone, "party": str(party), "table": table, "memo": memo,
}
# 파일 끝에 줄바꿈이 없으면 보충한 뒤 추가
f.seek(0, 2)
if f.tell() > 0:
f.seek(f.tell() - 1)
if f.read(1) not in ("\n", "\r"):
f.write("\n")
csv.DictWriter(f, fieldnames=FIELDS, lineterminator="\n").writerow(new)
return {"added": new}
if __name__ == "__main__":
mcp.run()
I built the reservation server and tested it on a copy of the ledger. Both tasks work: looking up bookings, and adding new ones that save to the CSV correctly. Your real `reservations.csv` hasn't been changed. **Files** - `server.py`: the MCP server. It runs on your Mac only, with no screen or login, using fastmcp from `../.venv`. Nothing new was installed. - `run.txt`: `../.venv/bin/python server.py` **What Claude can do** 1. **`find_reservations`**: finds bookings by date (`2026-10-21`), by name, or both. Part of a name works too, so "민수" finds 김민수. Results come back sorted by date and time. 2. **`add_reservation`**: adds a booking with date, time, name, phone, party size, and optionally table and memo. - New bookings get the next ID automatically (R-0006, R-0007, …). - Times are tidied up, so `9:30` is saved as `09:30`. Table names are capitalised (`t3` becomes `T3`). Line breaks in the memo are removed. - It won't book a table that's already taken at the same date and time. It names the existing booking and asks you to pick another table or time. - Wrong dates, impossible times like 25:00, and a party size of 0 are rejected with a message saying what's wrong. **What I tested** - Search by name and by date both return the right bookings. - A booking for T3 at 18:00 on 10/20 was refused, because 김민수 already has it. - A new booking was saved as R-0006, and a memo containing a comma kept its quotes in the CSV. - Wrong dates and times and a party size of 0 all gave clear error messages. To make Claude Code use it, register it once from this folder (I haven't done this, since you didn't ask): ``` claude mcp add reservations -- /Users/euiseong/aiskillwiki-lab/claude-mcp-server-dev/.venv/bin/python /Users/euiseong/aiskillwiki-lab/claude-mcp-server-dev/plug2/server.py ```