mcp 서버를 opencode로 테스트 하다가 claude 를 쓸 수 있어서 바꿔서 테스트 했다.
만드는 방식은 동일하니, 양쪽다 쓸수 있다.
하지만 역시, claude 쪽이 뭔가 좀더 깔끔한 느낌이긴하다. 역시 상용이 좋..;
mcp 서버 생성은 동일하게 하면 된다.
** 준비물 **
DART 의 API 키
dart 사이트에 회원가입후, 오픈API 키를 받는다.
2~3영업일 걸릴수 있다고 써 있지만, 보통 바로 되는것 같다.
전자공시시스템
많이 본 문서 최근 3영업일 기준 가장 많이 본 공시를 보여줍니다.
dart.fss.or.kr
그리고 직접 API 호출이 아닌 OpenDartReader 라는 파이썬 라이브러리를 활용하도록 한다.
1. 환경구성
Dart-MCP 폴더 생성
mkdir Dart-MCP
프로젝트 초기 화 및 필요한 라이브러리 설치
uv init
uv add opendartreader fastmcp
[project]
name = "dart-mcp"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"fastmcp>=3.2.3",
"opendartreader>=0.2.3",
]
.env 파일을 만들고 키를 써준다.
DART_API_KEY="DART키"
2. main.py 를 다음과 같이 수정한다.
import OpenDartReader
import pandas as pd
import os
from fastmcp import FastMCP
from typing import Annotated, Literal
from pydantic import Field
from dotenv import load_dotenv
load_dotenv()
# OpenDartReader uses relative path 'docs_cache' — ensure cwd is project root
os.chdir(os.path.dirname(os.path.abspath(__file__)))
mcp = FastMCP("Dart-MCP")
dart = OpenDartReader(os.getenv("DART_API_KEY"))
REPORT_CODES = [
'조건부자본증권미상환', '미등기임원보수', '회사채미상환', '단기사채미상환', '기업어음미상환',
'채무증권발행', '사모자금사용', '공모자금사용', '임원전체보수승인', '임원전체보수유형',
'주식총수', '회계감사', '감사용역', '회계감사용역계약', '사외이사', '신종자본증권미상환',
'증자', '배당', '자기주식', '최대주주', '최대주주변동', '소액주주', '임원', '직원',
'임원개인보수', '임원전체보수', '개인별보수', '타법인출자'
]
EVENT_CODES = [
'부도발생', '영업정지', '회생절차', '해산사유', '유상증자', '무상증자', '유무상증자', '감자',
'관리절차개시', '소송', '해외상장결정', '해외상장폐지결정', '해외상장', '해외상장폐지',
'전환사채발행', '신주인수권부사채발행', '교환사채발행', '관리절차중단', '조건부자본증권발행',
'자산양수도', '타법인증권양도', '유형자산양도', '유형자산양수', '타법인증권양수', '영업양도',
'영업양수', '자기주식취득신탁계약해지', '자기주식취득신탁계약체결', '자기주식처분', '자기주식취득',
'주식교환', '회사분할합병', '회사분할', '회사합병', '사채권양수', '사채권양도결정'
]
@mcp.tool(
name="get_corp_code",
description="공시대상회사의 고유번호(corp_code)를 반환하는 함수입니다.",
)
def get_corp_code(
corp_name: Annotated[str, Field(description="공시대상회사의 이름입니다.")]
) -> str:
"""
Args:
corp_name (str): 공시대상회사의 이름입니다.
Returns:
str: 공시대상회사의 고유번호(corp_code)입니다.
"""
return dart.find_corp_code(corp_name)
@mcp.tool(
name="get_company_overview",
description="공시대상회사의 개요를 반환하는 함수입니다.",
)
def get_company_overview(
corp_code: Annotated[str, Field(description="공시대상회사의 고유번호입니다.")]
) -> dict:
"""
Args:
corp_code (str): 공시대상회사의 고유번호입니다.
Returns:
dict: 공시대상회사의 개요 정보입니다.
"""
return dart.company(corp_code)
@mcp.tool(
name="get_financial_statement",
description="공시대상회사의 재무제표를 반환하는 함수입니다.",
)
def get_financial_statement(
corp_code: Annotated[str, Field(description="공시대상회사의 고유번호입니다.")],
date: Annotated[str, Field(description="재무제표의 날짜입니다. YYYY 형식입니다.")],
report_code: Annotated[str, Field(description="재무제표의 종류입니다. 11013: 연결재무제표, 11012: 별도재무제표")],
sj_div: Annotated[Literal['BS','IS'], Field(description="재무제표의 구분입니다. BS: 재무상태표, IS: 손익계산서")],
) -> pd.DataFrame:
"""
Args:
corp_code (str): 공시대상회사의 고유번호입니다.
date (str): 재무제표의 날짜입니다. YYYY 형식입니다.
report_code (str): 재무제표의 종류입니다. 11013: 연결재무제표, 11012: 별도재무제표
sj_div (str): 재무제표의 구분입니다. BS: 재무상태표, IS: 손익계산서
Returns:
pd.DataFrame: 공시대상회사의 재무제표 정보입니다.
"""
df = dart.finstate(corp_code, date, report_code)
filtered_df = df[(df['fs_div'] == 'CFS') & (df['sj_div'] == sj_div)]
if filtered_df.empty:
filtered_df = df[(df['fs_div'] == 'OFS') & (df['sj_div'] == sj_div)]
filtered_df = filtered_df[["corp_code", "bsns_year", "reprt_code", "account_nm", "thstrm_amount"]]
return filtered_df
@mcp.tool(
name="get_specific_business_report",
description="공시대상회사의 특정 사업보고서 항목을 반환하는 함수",
)
def get_specific_business_report(
corp_code: Annotated[str, Field(description="공시대상회사의 고유번호입니다.")],
report_code: Annotated[str, Field(description="사업보고서의 종류입니다. 11013: 연결재무제표, 11012: 별도재무제표")],
date: Annotated[str, Field(description="사업보고서의 날짜입니다. YYYY 형식입니다.")],
) -> pd.DataFrame:
"""
Args:
corp_code (str): 공시대상회사의 고유번호입니다.
report_code (str): 사업보고서의 종류입니다. 11013: 연결재무제표, 11012: 별도재무제표
date (str): 사업보고서의 날짜입니다. YYYY 형식입니다.
Returns:
pd.DataFrame: 공시대상회사의 특정 사업보고서 항목 정보입니다.
"""
if report_code not in REPORT_CODES:
return {"error": "유효하지 않은 report_code입니다. 유효한 report_code는 다음과 같습니다: " + ", ".join(REPORT_CODES)}
result = dart.report(corp_code, report_code, date)
if isinstance(result, pd.DataFrame) and result.empty:
return {"error": "해당 report_code에 대한 데이터가 없습니다."}
return result
@mcp.tool(
name="get_major_event_report",
description="공시대상회사의 주요 이벤트 보고서를 반환하는 함수",
)
def get_major_event_report(
corp_code: Annotated[str, Field(description="공시대상회사의 고유번호입니다.")],
event: Annotated[str, Field(description="이벤트 보고서의 종류입니다. 이벤트 코드중에 하나여야합니다. {EVENT_CODES}")],
date: Annotated[str, Field(description="이벤트 보고서의 날짜입니다. YYYY 형식입니다.")],
) :
"""
Args:
corp_code (str): 공시대상회사의 고유번호입니다.
event_code (str): 이벤트 보고서의 종류입니다. 부도발생, 영업정지, 회생절차 등
date (str): 이벤트 보고서의 날짜입니다. YYYY 형식입니다.
Returns:
dict or pd.DataFrame: 공시대상회사의 주요 이벤트 보고서 정보입니다. 이벤트가 유효하지 않거나 데이터가 없으면 에러를 반환합니다.
"""
if event not in EVENT_CODES:
return {"error": "유효하지 않은 event_code입니다. 유효한 event_code는 다음과 같습니다: " + ", ".join(EVENT_CODES)}
result = dart.event(corp_code, event, date)
if isinstance(result, pd.DataFrame) and result.empty:
return {"error": "해당 event_code에 대한 데이터가 없습니다."}
return result
if __name__ == "__main__":
mcp.run()
dotenv 모듈은 별도로 설치 안해도 됐었다.(fastmcp에 포함되어있나?)
처음 mcp 모듈 실행시 폴더 권한 오류가 발생하는데, 모듈 폴더에 docs_cache 를 미리 생성해 두자. (관련 소스 라인 참고!)
3. 테스트 실행
환경 엑티베이트 하고
.venv\Script\activate.bat
fastmcp 로 실행해서 정상실행을 확인한다.
fastmcp run main.py
정상이면, claude config 에 mcp 서버로 등록한다.
4. mcp 모듈 등록
클로드에게
"이 mcp 서버를 mcp 모듈로 등록해줘"
라고 해도 잘 등록해준다.
fastmcp의 기능으로 아래와 같이 실행하면
fastmcp install claude-desktop main.py
되어야 될것 같은데, 나는 안됐다. config 를 못찾는다고 나온다.
클로드가 이렇게 등록해 줬다.
C:\Users\{사용자ID}\AppData\Local\Packages\Claude_pzs8sxrjxfjjc\LocalCache\Roaming\Claude\claude_desktop_config.json
"dart-mcp": {
"command": "C:\\MENDIX_DEV\\MCP\\Dart-MCP\\.venv\\Scripts\\fastmcp.exe",
"args": [
"run",
"C:\\MENDIX_DEV\\MCP\\Dart-MCP\\main.py"
],
"env": {
"DART_API_KEY": "API키"
}
}
아래와 같이 활용 가능하다.

xx DART 고유번호를 조회해줘
xx 기업정보를 조회해줘
xx 2025년 4분기 재무상태표/손익계산서 정보를 조회해줘
xx 2025년 배당 관련 사업보고서 항목을 조회해줘
등의 다양한 질문이 가능하다.