We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/ChangooLee/mcp-opendart'
If you have feedback or need assistance with the MCP directory API, please join our Discord server
import logging, datetime
from typing import Any, Optional, Annotated
from pydantic import Field
from mcp_opendart.server import mcp
from mcp.types import TextContent
from mcp_opendart.utils.ctx_helper import with_context, with_context_async, as_json_text
from mcp_opendart.registry.initialize_registry import initialize_registry
logger = logging.getLogger("mcp-opendart")
tool_registry = initialize_registry()
@mcp.tool(
name="get_opendart_tool_info",
description="기업의 심층 분석을 위해 mcp 도구의 이름을 정확히 입력하여 도구의 목적, 활용 흐름, 필수 파라미터, 연관 도구 등을 확인 후 진행, 예: get_opendart_tool_info(tool_name='get_corporation_info')",
tags={"심층분석", "기업분석", "도구흐름"}
)
async def get_opendart_tool_info(
tool_name: str,
) -> TextContent:
tool = tool_registry.get_tool(tool_name)
if not tool:
return TextContent(type="text", text=f"❌ '{tool_name}'이라는 이름의 도구를 찾을 수 없습니다.")
# 파라미터 설명 추출
param_lines = []
props = tool.parameters.get("properties", {})
for param, meta in props.items():
desc = meta.get("description", "")
param_lines.append(f"- {param}: {desc}")
# linked 도구 리스트
linked = tool.linked_tools or []
linked_str = ", ".join(linked) if linked else "(연관 도구 없음)"
# 현재 날짜
today = datetime.date.today().strftime("%Y-%m-%d")
# 응답 생성
text = f"""
📌 {tool.name} ({tool.korean_name or '도구명 미정'})
이 도구는 특정 기업의 "{tool.name}"을(를) 다음과 같은 파라미터를 이용하여 조회할 수 있습니다:
{chr(10).join(param_lines)}
날짜가 필요한 경우, 반드시 시스템 기준일({today})을 기준으로 조회를 시작해야 합니다.
이 정보를 바탕으로, 사용자는 다음과 같은 흐름으로 도구를 활용할 수 있습니다:
1. 먼저 get_corporation_code_by_name 도구를 통해 기업명을 고유번호로 변환합니다.
2. {tool.name}을 사용해 관련 정보를 조회합니다.
3. 연관된 다음 도구를 반드시 확인해야 합니다:
{linked_str}
이 도구는 정보 흐름의 중심 축으로 활용되며, 다른 도구들과 결합되어 기업 리스크 분석에 핵심적인 역할을 합니다.
"""
return TextContent(type="text", text=text.strip())
@mcp.tool(
name="get_corporation_code_by_name",
description="기업명을 이용하여 기업 고유번호 조회, 공시조회를 위해 가장 먼저 실행하여 고유번호를 얻어야 함",
tags={"기업검색", "고유번호", "기업기초정보", "기업식별"}
)
async def get_corporation_code_by_name(
corp_name: Annotated[str, "기업명 (예: 삼성전자, 현대자동차)"],
) -> TextContent:
result = await with_context_async(None, "get_corporation_code_by_name", lambda context: context.ds001.get_corporation_code_by_name(corp_name))
return as_json_text(result)
@mcp.tool(
name="get_disclosure_list",
description="기업의 전체 공시 이력을 날짜별로 조회하여 경영활동, 재무현황, 지배구조 변화를 신속하게 파악. 여러 페이지가 있을 경우 페이지 번호를 바꿔 조회",
tags={"공시", "목록", "접수내역", "이벤트탐지"}
)
def get_disclosure_list(
corp_code: Annotated[str, "공시대상회사의 고유번호 (8자리)"],
bgn_de: Annotated[str, "조회 시작일 (YYYYMMDD)"],
end_de: Annotated[str, "조회 종료일 (YYYYMMDD)"],
page_no: Annotated[int, "페이지 번호 (기본값: 1)"] = 1,
pblntf_ty: Annotated[str, "공시유형 (A:정기공시, B:주요사항, C:발행, D:지분, E:기타 등)"] = "",
) -> TextContent:
"""
공시 목록 조회 - 기업의 전체 공시 이력을 날짜별로 조회하여 경영활동, 재무현황, 지배구조 변화를 신속하게 파악
응답에 total_count와 total_page 정보가 포함됩니다.
여러 페이지가 있을 경우 page_no 파라미터를 사용하여 다른 페이지를 조회하세요.
예: page_no=2, page_no=3 등으로 순차적으로 조회
Args:
corp_code (str): 공시대상회사의 고유번호 (8자리)
bgn_de (str): 조회 시작일 (YYYYMMDD)
end_de (str): 조회 종료일 (YYYYMMDD)
page_no (int): 페이지 번호 (기본값: 1) - total_page를 확인하여 필요한 페이지를 조회
pblntf_ty (str): 공시유형 필터 (기본값: "" - 모든 공시유형)
- "": 모든 공시유형 (기본값)
- "A": 정기공시 (사업보고서, 반기보고서, 분기보고서, 반기보고서 등)
- "B": 주요사항보고 (영업정지, 감자, 합병, 분할, 영업양수도 등)
- "C": 발행공시 (신주인수권부사채, 전환사채, 신주발행 등)
- "D": 지분공시 (대량보유상황보고서, 임원변경 등)
- "E": 기타공시 (기업설명회, 투자설명서 등)
- "F": 외부감사관련 (감사보고서, 회계법인 변경 등)
- "G": 펀드공시 (펀드 관련 공시)
- "H": 자산유동화 (자산유동화 관련 공시)
- "I": 거래소공시 (거래소 관련 공시)
- "J": 공정위공시 (공정거래위원회 관련 공시)
공시유형 선택 가이드:
- 사업보고서, 반기보고서, 분기보고서 조회 시: "A" 사용
- 경영진 변경, 사업 구조 변경 등 중요 사항: "B" 사용
- 신주발행, 사채발행 등 자금조달: "C" 사용
- 주주변동, 대주주 변경: "D" 사용
- IR 활동, 투자설명회: "E" 사용
참고: https://opendart.fss.or.kr/guide/detail.do?apiGrpCd=DS001&apiId=2019001
"""
result = with_context(None, "get_disclosure_list", lambda context: context.ds001.get_disclosure_list(
corp_code=corp_code,
bgn_de=bgn_de,
end_de=end_de,
page_no=page_no,
pblntf_ty=pblntf_ty
))
return as_json_text(result)
@mcp.tool(
name="get_corporation_info",
description="대표자, 결산월, 상장상태 등 기업 기본 정보 기반 지배구조 및 공시 일정 분석",
tags={"기업기초정보", "지배구조", "공시일정", "대표자분석", "가족경영"}
)
def get_corporation_info(
corp_code: Annotated[str, "공시대상회사의 고유번호 (8자리)"],
) -> TextContent:
"""
기업 개황정보 조회
Args:
corp_code (str): 공시대상회사의 고유번호(8자리)
참고: https://opendart.fss.or.kr/guide/detail.do?apiGrpCd=DS001&apiId=2019002
"""
result = with_context(None, "get_corporation_info", lambda context: context.ds001.get_corporation_info(corp_code))
return as_json_text(result)
@mcp.tool(
name="get_disclosure_document",
description="공시서류 원본파일을 다운로드하고 자동으로 재무제표 주석을 추출하여 캐싱합니다. 재무제표 주석이 없으면 다른 보고서를 찾아보라는 안내를 제공합니다.",
tags={"공시서류", "원본파일", "재무제표주석", "자동추출", "캐싱"}
)
def get_disclosure_document(
rcp_no: Annotated[str, "공시서류의 접수번호 (14자리)"],
) -> TextContent:
"""
공시서류 원본파일 다운로드 및 자동 재무제표 주석 추출
Args:
rcp_no (str): 공시서류의 접수번호 (14자리)
Returns:
TextContent: 공시서류 데이터 및 재무제표 주석 추출 결과
Features:
- XML 파일 다운로드 및 로컬 저장
- 자동 재무제표 주석 추출 (연결재무제표 주석, 재무제표 주석)
- 재무제표 주석이 없으면 다른 보고서를 찾아보라는 안내 제공
- 구조화된 데이터 캐싱 (disclosure_cache 디렉토리)
참고: https://opendart.fss.or.kr/guide/detail.do?apiGrpCd=DS001&apiId=2019003
"""
# 1. 공시서류 다운로드
result = with_context(None, "get_disclosure_document", lambda context: context.ds001.get_disclosure_document(rcp_no))
# 2. 재무제표 주석 자동 추출 (다운로드 성공 시)
if result.get("status_code") == "000" or result.get("status") == "000":
try:
financial_notes_result = with_context(None, "extract_financial_notes_document",
lambda context: context.ds001.extract_financial_notes_document(rcp_no))
if financial_notes_result.get("status") == "000":
result["financial_notes_extraction"] = {
"status": "success",
"message": "재무제표 주석이 성공적으로 추출되었습니다",
"summary": financial_notes_result.get("summary", {}),
"sections": financial_notes_result.get("sections", {}),
"save_path": financial_notes_result.get("save_path", ""),
"extraction_date": financial_notes_result.get("extraction_date", ""),
"final_rcp_no": financial_notes_result.get("rcp_no", rcp_no)
}
else:
result["financial_notes_extraction"] = {
"status": "error",
"message": financial_notes_result.get("message", "재무제표 주석 추출 실패"),
"error": financial_notes_result.get("error", "알 수 없는 오류")
}
except Exception as e:
result["financial_notes_extraction"] = {
"status": "error",
"message": "재무제표 주석 추출 중 오류 발생",
"error": str(e)
}
return as_json_text(result)
@mcp.tool(
name="search_financial_notes",
description="추출된 재무제표 주석, 사업의 내용, 회사의 개요 데이터에서 테이블과 문단을 검색합니다. 연결재무제표 주석, 재무제표 주석, 사업의 내용, 회사의 개요에서 특정 키워드를 검색할 수 있습니다.",
tags={"재무제표주석", "검색", "테이블검색", "문단검색", "키워드검색", "사업의내용", "회사의개요"}
)
def search_financial_notes(
rcp_no: Annotated[str, Field(description="공시 접수번호 (예: 20241231000420). get_disclosure_list에서 조회 가능")],
search_term: Annotated[str, Field(description="검색할 키워드 (예: CSM, 계약서비스마진, 보험계약자산)")],
section_type: Annotated[str, Field(description="검색 섹션 (all: 전체, consolidated_notes: 연결재무제표 주석, separate_notes: 재무제표 주석, business_content: 사업의 내용, company_overview: 회사의 개요)")] = "all",
search_in: Annotated[str, Field(description="검색 범위 (both: 테이블과 문단 모두, tables: 테이블만, paragraphs: 문단만)")] = "both",
case_sensitive: Annotated[bool, Field(description="대소문자 구분 여부 (기본값: False)")] = False,
max_results: Annotated[int, Field(description="최대 결과 수 (기본값: 50)")] = 50,
) -> TextContent:
"""
재무제표 주석 데이터 검색
Args:
rcp_no (str): 공시서류의 접수번호 (14자리)
search_term (str): 검색할 키워드
section_type (str): 검색할 섹션 ("all", "consolidated_notes", "separate_notes", "business_content", "company_overview")
search_in (str): 검색 대상 ("both", "tables", "paragraphs")
case_sensitive (bool): 대소문자 구분 여부 (기본값: False)
max_results (int): 최대 결과 수 (기본값: 50)
Returns:
TextContent: 검색 결과 (테이블 및 문단)
"""
from pathlib import Path
import json
import re
from difflib import SequenceMatcher
def is_similar_match(search_term: str, content: str) -> bool:
"""유사도 20% 이상 매칭 확인"""
# 정확한 매칭 먼저 확인
if case_sensitive:
if search_term in content:
return True
else:
if search_term.lower() in content.lower():
return True
# 유사도 매칭 확인 (20% 이상)
similarity = SequenceMatcher(None, search_term.lower(), content.lower()).ratio()
return similarity >= 0.2
try:
# 캐시 디렉토리 확인
project_root = Path(__file__).parent.parent.parent
cache_path = project_root / 'mcp_opendart' / 'utils' / 'data' / 'disclosure_cache' / f'financial_notes_{rcp_no}'
if not cache_path.exists():
return as_json_text({
"status": "404",
"message": "재무제표 주석 데이터를 찾을 수 없습니다. 먼저 get_disclosure_document를 실행하여 데이터를 추출하세요.",
"error": f"캐시 경로가 존재하지 않습니다: {cache_path}"
})
# 검색 결과 저장
search_results = {
"status": "000",
"message": "정상",
"rcp_no": rcp_no,
"search_term": search_term,
"section_type": section_type,
"search_in": search_in,
"results": {
"tables": [],
"paragraphs": []
}
}
# 섹션별 검색 (자동 확장 포함)
sections_to_search = []
if section_type == "all":
sections_to_search = ["consolidated_notes", "separate_notes", "business_content", "company_overview"]
elif section_type in ["consolidated_notes", "separate_notes", "business_content", "company_overview"]:
sections_to_search = [section_type]
# 검색 결과 풍부화를 위한 자동 확장 로직
enriched_sections = set(sections_to_search)
# 매칭된 섹션의 상위 디렉토리 자동 포함
for section in sections_to_search:
if section == "business_content":
# business_content의 모든 하위 섹션 자동 포함
enriched_sections.add("business_content")
elif section == "company_overview":
# company_overview의 모든 하위 섹션 자동 포함
enriched_sections.add("company_overview")
sections_to_search = list(enriched_sections)
for section_name in sections_to_search:
section_path = cache_path / section_name
if not section_path.exists():
continue
# 사업의 내용과 회사의 개요 섹션은 하위 구조가 다름
if section_name in ["business_content", "company_overview"]:
# 하위 섹션들 검색
subsections_path = section_path / 'subsections'
if subsections_path.exists():
for subsection_dir in subsections_path.iterdir():
if subsection_dir.is_dir():
# 하위 섹션 메타데이터 읽기
try:
with open(subsection_dir / 'metadata.json', 'r', encoding='utf-8') as f:
subsection_meta = json.load(f)
subsection_name = subsection_meta.get('original_name', subsection_dir.name)
except:
subsection_name = subsection_dir.name
# 🎯 섹션명 자체도 검색 대상에 포함
section_name_match = False
if case_sensitive:
section_name_match = search_term in subsection_name
else:
section_name_match = search_term.lower() in subsection_name.lower()
# 🎯 섹션명이 매칭되면 해당 섹션의 모든 내용을 포함
if section_name_match:
# 섹션명 매칭 시 모든 테이블과 문단을 결과에 포함
if search_in in ["tables", "both"]:
tables_path = subsection_dir / 'tables'
if tables_path.exists():
for table_file in tables_path.glob('*.json'):
try:
with open(table_file, 'r', encoding='utf-8') as f:
table_data = json.load(f)
search_text = ' '.join(table_data.get('headers', []) +
[cell for row in table_data.get('rows', []) for cell in row])
search_results["results"]["tables"].append({
"section": f"{section_name}/{subsection_name}",
"table_id": table_data.get('table_id'),
"file_path": str(table_file),
"headers": table_data.get('headers', []),
"row_count": len(table_data.get('rows', [])),
"matched_content": search_text,
"context_type": "사업정보" if section_name == "business_content" else "기본정보" if section_name == "company_overview" else "재무정보",
"match_type": "section_name"
})
except Exception as e:
continue
if search_in in ["paragraphs", "both"]:
paragraphs_path = subsection_dir / 'paragraphs'
if paragraphs_path.exists():
for para_file in paragraphs_path.glob('*.json'):
try:
with open(para_file, 'r', encoding='utf-8') as f:
para_data = json.load(f)
content = para_data.get('content', '')
search_results["results"]["paragraphs"].append({
"section": f"{section_name}/{subsection_name}",
"para_id": para_data.get('para_id'),
"file_path": str(para_file),
"line_number": para_data.get('line_number'),
"content": content,
"context_type": "사업정보" if section_name == "business_content" else "기본정보" if section_name == "company_overview" else "재무정보",
"match_type": "section_name"
})
except Exception as e:
continue
else:
# 섹션명이 매칭되지 않으면 내용에서만 검색
# 테이블 검색
if search_in in ["tables", "both"]:
tables_path = subsection_dir / 'tables'
if tables_path.exists():
for table_file in tables_path.glob('*.json'):
try:
with open(table_file, 'r', encoding='utf-8') as f:
table_data = json.load(f)
# 테이블 내용에서 검색
search_text = ' '.join(table_data.get('headers', []) +
[cell for row in table_data.get('rows', []) for cell in row])
# 내용 매칭 확인 (유사도 80% 포함)
content_match = is_similar_match(search_term, search_text)
if content_match:
search_results["results"]["tables"].append({
"section": f"{section_name}/{subsection_name}",
"table_id": table_data.get('table_id'),
"file_path": str(table_file),
"headers": table_data.get('headers', []),
"row_count": len(table_data.get('rows', [])),
"matched_content": search_text,
"context_type": "사업정보" if section_name == "business_content" else "기본정보" if section_name == "company_overview" else "재무정보",
"match_type": "content"
})
except Exception as e:
continue
# 문단 검색
if search_in in ["paragraphs", "both"]:
paragraphs_path = subsection_dir / 'paragraphs'
if paragraphs_path.exists():
for para_file in paragraphs_path.glob('*.json'):
try:
with open(para_file, 'r', encoding='utf-8') as f:
para_data = json.load(f)
content = para_data.get('content', '')
# 내용 매칭 확인 (유사도 80% 포함)
content_match = is_similar_match(search_term, content)
if content_match:
search_results["results"]["paragraphs"].append({
"section": f"{section_name}/{subsection_name}",
"para_id": para_data.get('para_id'),
"file_path": str(para_file),
"line_number": para_data.get('line_number'),
"content": content,
"context_type": "사업정보" if section_name == "business_content" else "기본정보" if section_name == "company_overview" else "재무정보",
"match_type": "content"
})
except Exception as e:
continue
else:
# 기존 재무제표 주석 섹션 검색
# 테이블 검색
if search_in in ["tables", "both"]:
tables_path = section_path / 'tables'
if tables_path.exists():
for table_file in tables_path.glob('*.json'):
try:
with open(table_file, 'r', encoding='utf-8') as f:
table_data = json.load(f)
# 테이블 내용에서 검색
search_text = ' '.join(table_data.get('headers', []) +
[cell for row in table_data.get('rows', []) for cell in row])
if is_similar_match(search_term, search_text):
search_results["results"]["tables"].append({
"section": section_name,
"table_id": table_data.get('table_id'),
"file_path": str(table_file),
"headers": table_data.get('headers', []),
"row_count": len(table_data.get('rows', [])),
"matched_content": search_text,
"context_type": "재무정보"
})
except Exception as e:
continue
# 문단 검색
if search_in in ["paragraphs", "both"]:
paragraphs_path = section_path / 'paragraphs'
if paragraphs_path.exists():
for para_file in paragraphs_path.glob('*.json'):
try:
with open(para_file, 'r', encoding='utf-8') as f:
para_data = json.load(f)
content = para_data.get('content', '')
if is_similar_match(search_term, content):
search_results["results"]["paragraphs"].append({
"section": section_name,
"para_id": para_data.get('para_id'),
"file_path": str(para_file),
"line_number": para_data.get('line_number'),
"content": content,
"context_type": "재무정보"
})
except Exception as e:
continue
# 결과 수 제한
search_results["results"]["tables"] = search_results["results"]["tables"][:max_results]
search_results["results"]["paragraphs"] = search_results["results"]["paragraphs"][:max_results]
# 컨텍스트별 그룹핑 정보 추가
context_groups = {}
for result in search_results["results"]["paragraphs"]:
context_type = result.get("context_type", "기타")
if context_type not in context_groups:
context_groups[context_type] = 0
context_groups[context_type] += 1
for result in search_results["results"]["tables"]:
context_type = result.get("context_type", "기타")
if context_type not in context_groups:
context_groups[context_type] = 0
context_groups[context_type] += 1
# 통계 추가
search_results["summary"] = {
"total_tables_found": len(search_results["results"]["tables"]),
"total_paragraphs_found": len(search_results["results"]["paragraphs"]),
"sections_searched": sections_to_search,
"context_groups": context_groups,
"enrichment_applied": True
}
return as_json_text(search_results)
except Exception as e:
return as_json_text({
"status": "500",
"message": "검색 중 오류 발생",
"error": str(e)
})
@mcp.tool(
name="get_corporation_code",
description="OpenDART에서 제공하는 모든 공시대상 회사의 고유번호 전체 목록(XML 파일)을 조회합니다. 기업명 검색 또는 고유번호 매핑에 사용됩니다.",
tags={"기업전체목록", "고유번호전체", "기업식별", "코드매핑"}
)
def get_corporation_code() -> TextContent:
"""
고유번호 목록 조회
Returns:
Dict[str, Any]: 고유번호 목록 (기업명, 고유번호, 종목코드 등 포함)
참고: https://opendart.fss.or.kr/guide/detail.do?apiGrpCd=DS001&apiId=2019018
"""
result = with_context(None, "get_corporation_code", lambda context: context.ds001.get_corporation_code())
return as_json_text(result)