├── .python-version ├── .DS_Store ├── .gitignore ├── pyproject.toml ├── README.md ├── uv.lock └── dart.py /.python-version: -------------------------------------------------------------------------------- 1 | 3.10 2 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/dart-mcp/main/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python-generated files 2 | __pycache__/ 3 | *.py[oc] 4 | build/ 5 | dist/ 6 | wheels/ 7 | *.egg-info 8 | 9 | # Virtual environments 10 | .venv 11 | .env 12 | local.env -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "dart-mcp" 3 | version = "0.1.0" 4 | description = "Add your description here" 5 | readme = "README.md" 6 | requires-python = ">=3.10" 7 | dependencies = [ 8 | "httpx>=0.28.1", 9 | "mcp[cli]>=1.6.0", 10 | "python-dotenv>=1.0.0", 11 | ] 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DART-MCP: 재무 분석을 위한 Claude 확장 프로그램 2 | 3 | DART API를 활용한 재무 분석 MCP(Model-assisted Capability Package)입니다. Claude를 이용하여 상장 기업의 재무 데이터를 쉽게 분석하고 시각화할 수 있습니다. 4 | 5 | ## 가능한 것 / 불가능한 것 6 | 7 | ### 가능한 것 (O) 8 | - 주요 재무 분석 9 | - 상세 재무 분석 10 | - 기업의 사업부별 매출 11 | - 클로드를 이용한 시각화 12 | - 재무지표를 활용한 벨류에이션 (DCF 등) 13 | 14 | ### 불가능한 것 (X) 15 | - 주가 및 시가총액 제공 16 | - 해외기업 분석 17 | - 클로드 무료 사용량 이상의 사용 18 | - 한 채팅창에서 다량 사용 (잘 안되면 채팅창 새로 만들어서 쓰기) 19 | - 100% 정확한 정보 20 | 21 | **제공하는 투자 정보는 실제와 다를 수 있고 투자 책임은 투자한 본인에게 있습니다.** 22 | 23 | ## 사용 예시 24 | 25 | ### 재무 데이터 분석 및 시각화 26 | ``` 27 | 파마리서치의 2023, 2024년 매출액, 영업이익 추이 분기별로 그래프로 보여줘. 그리고 매출비중이 어떻게 되는지 알려줘. 영업이익이나 매출액 변동 이유도 분석해줘. 28 | ``` 29 | 30 | ### 기업 비교 분석 31 | ``` 32 | 카카오와 네이버 2024년 수익성지표를 비교해서 분기별로 보여주고, 각 기업들은 어떤 사업부가 성장을 이끌지 알려줘. 33 | ``` 34 | 35 | ### 재무 위험 평가 36 | ``` 37 | 한국전력의 최근 부채상황을 조사하고, 상세하게 어떤 부분이 문제인지 분석해줘. 38 | ``` 39 | 40 | ## 사전 준비 41 | 42 | ### DART API 키 발급 43 | 1. [DART 오픈API](https://opendart.fss.or.kr) 웹사이트에 접속 44 | 2. 회원가입 및 로그인 45 | 3. [인증키 신청/관리] - [오픈API 이용 신청] 메뉴 클릭 46 | 4. 이용정보 입력 후 신청 47 | 5. [인증키 신청/관리] - [오픈API 이용현황] 메뉴에서 발급된 인증키 확인 48 | 49 | ### Claude 데스크톱 앱 설치 50 | 1. [Claude 데스크톱 앱](https://claude.ai/desktop) 다운로드 51 | 2. 계정 가입 및 로그인 52 | 53 | ## 설치 방법 54 | 55 | ### 1. GitHub에서 프로젝트 다운로드 56 | GitHub 페이지에서 zip 파일을 다운로드합니다. 57 | https://github.com/2geonhyup/dart-mcp 58 | 59 | ### 2. ZIP 파일 압축 해제 및 폴더 위치 확인 60 | 1) 다운로드한 ZIP 파일의 압축을 해제합니다. 61 | 2) 압축 해제 폴더가 **Downloads**에 있는지 확인합니다. **다른 위치에 있다면 Downloads 위치로 옮겨주세요.** 62 | 63 | ### 3. 폴더 이름 변경 64 | 압축 해제한 폴더 dart-mcp-main 이름을 **dart-mcp로 반드시 바꿔주세요.** (처음부터 dart-mcp라면 바꾸지 마세요) 65 | 66 | ### 4. Claude 앱 접속 및 설정 접근 67 | 1) 설치한 Claude 데스크톱 앱을 실행합니다. 68 | 2) 맥 사용자: **Claude > 설정 > 개발자 > 설정 편집** 클릭 69 | 윈도우 사용자: **설정 > 개발자 > 설정 편집** 클릭 70 | 71 | ### 5. 설정 파일 열기 72 | **상단 claude_desktop_config 파일**을 텍스트 편집기로 엽니다. 73 | 74 | ### 6. 설정 코드 입력 75 | 1. 먼저 알맞은 키와 이름을 입력하세요 76 | - DART API 키: 발급받은 API 키 입력 77 | - 컴퓨터 이름: 컴퓨터 계정 이름 입력 (Mac에서는 Finder 홈 폴더, Windows에서는 C:\사용자 폴더명) 78 | 79 | 2. 다음 코드를 이용하여 설정 파일에 입력 80 | ```json 81 | { 82 | "mcpServers": { 83 | "dart-mcp": { 84 | "command": "uv", 85 | "args": ["--directory", "/Users/{컴퓨터이름}/Downloads/dart-mcp", "run", "dart.py"], 86 | "env": { 87 | "DART_API_KEY": "{DART_API_KEY}" 88 | } 89 | } 90 | } 91 | } 92 | ``` 93 | 94 | ### 7. Claude 재시작 및 사용 시작 95 | **설정 파일을 저장하고 Claude 앱을 닫은 후 다시 시작**합니다. 96 | 이제 Claude에게 질문하면 DART API를 호출하여 답변을 제공합니다. 97 | 98 | ## 사용시 주의사항 99 | - 기업명은 공식적으로 상장된 이름으로 제공해야 합니다. 100 | - 코스피, 코스닥 종목만 조사 가능합니다. 101 | - 주가나 시가총액과 같은 실시간 정보들은 앞으로 연동할 계획입니다. -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | revision = 1 3 | requires-python = ">=3.10" 4 | 5 | [[package]] 6 | name = "annotated-types" 7 | version = "0.7.0" 8 | source = { registry = "https://pypi.org/simple" } 9 | sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } 10 | wheels = [ 11 | { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, 12 | ] 13 | 14 | [[package]] 15 | name = "anyio" 16 | version = "4.9.0" 17 | source = { registry = "https://pypi.org/simple" } 18 | dependencies = [ 19 | { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, 20 | { name = "idna" }, 21 | { name = "sniffio" }, 22 | { name = "typing-extensions", marker = "python_full_version < '3.13'" }, 23 | ] 24 | sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949 } 25 | wheels = [ 26 | { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916 }, 27 | ] 28 | 29 | [[package]] 30 | name = "certifi" 31 | version = "2025.1.31" 32 | source = { registry = "https://pypi.org/simple" } 33 | sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } 34 | wheels = [ 35 | { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, 36 | ] 37 | 38 | [[package]] 39 | name = "click" 40 | version = "8.1.8" 41 | source = { registry = "https://pypi.org/simple" } 42 | dependencies = [ 43 | { name = "colorama", marker = "sys_platform == 'win32'" }, 44 | ] 45 | sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } 46 | wheels = [ 47 | { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, 48 | ] 49 | 50 | [[package]] 51 | name = "colorama" 52 | version = "0.4.6" 53 | source = { registry = "https://pypi.org/simple" } 54 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } 55 | wheels = [ 56 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, 57 | ] 58 | 59 | [[package]] 60 | name = "dart-mcp" 61 | version = "0.1.0" 62 | source = { virtual = "." } 63 | dependencies = [ 64 | { name = "httpx" }, 65 | { name = "mcp", extra = ["cli"] }, 66 | { name = "python-dotenv" }, 67 | ] 68 | 69 | [package.metadata] 70 | requires-dist = [ 71 | { name = "httpx", specifier = ">=0.28.1" }, 72 | { name = "mcp", extras = ["cli"], specifier = ">=1.6.0" }, 73 | { name = "python-dotenv", specifier = ">=1.0.0" }, 74 | ] 75 | 76 | [[package]] 77 | name = "exceptiongroup" 78 | version = "1.2.2" 79 | source = { registry = "https://pypi.org/simple" } 80 | sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } 81 | wheels = [ 82 | { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, 83 | ] 84 | 85 | [[package]] 86 | name = "h11" 87 | version = "0.14.0" 88 | source = { registry = "https://pypi.org/simple" } 89 | sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } 90 | wheels = [ 91 | { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, 92 | ] 93 | 94 | [[package]] 95 | name = "httpcore" 96 | version = "1.0.8" 97 | source = { registry = "https://pypi.org/simple" } 98 | dependencies = [ 99 | { name = "certifi" }, 100 | { name = "h11" }, 101 | ] 102 | sdist = { url = "https://files.pythonhosted.org/packages/9f/45/ad3e1b4d448f22c0cff4f5692f5ed0666658578e358b8d58a19846048059/httpcore-1.0.8.tar.gz", hash = "sha256:86e94505ed24ea06514883fd44d2bc02d90e77e7979c8eb71b90f41d364a1bad", size = 85385 } 103 | wheels = [ 104 | { url = "https://files.pythonhosted.org/packages/18/8d/f052b1e336bb2c1fc7ed1aaed898aa570c0b61a09707b108979d9fc6e308/httpcore-1.0.8-py3-none-any.whl", hash = "sha256:5254cf149bcb5f75e9d1b2b9f729ea4a4b883d1ad7379fc632b727cec23674be", size = 78732 }, 105 | ] 106 | 107 | [[package]] 108 | name = "httpx" 109 | version = "0.28.1" 110 | source = { registry = "https://pypi.org/simple" } 111 | dependencies = [ 112 | { name = "anyio" }, 113 | { name = "certifi" }, 114 | { name = "httpcore" }, 115 | { name = "idna" }, 116 | ] 117 | sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } 118 | wheels = [ 119 | { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, 120 | ] 121 | 122 | [[package]] 123 | name = "httpx-sse" 124 | version = "0.4.0" 125 | source = { registry = "https://pypi.org/simple" } 126 | sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 } 127 | wheels = [ 128 | { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 }, 129 | ] 130 | 131 | [[package]] 132 | name = "idna" 133 | version = "3.10" 134 | source = { registry = "https://pypi.org/simple" } 135 | sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } 136 | wheels = [ 137 | { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, 138 | ] 139 | 140 | [[package]] 141 | name = "markdown-it-py" 142 | version = "3.0.0" 143 | source = { registry = "https://pypi.org/simple" } 144 | dependencies = [ 145 | { name = "mdurl" }, 146 | ] 147 | sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } 148 | wheels = [ 149 | { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, 150 | ] 151 | 152 | [[package]] 153 | name = "mcp" 154 | version = "1.6.0" 155 | source = { registry = "https://pypi.org/simple" } 156 | dependencies = [ 157 | { name = "anyio" }, 158 | { name = "httpx" }, 159 | { name = "httpx-sse" }, 160 | { name = "pydantic" }, 161 | { name = "pydantic-settings" }, 162 | { name = "sse-starlette" }, 163 | { name = "starlette" }, 164 | { name = "uvicorn" }, 165 | ] 166 | sdist = { url = "https://files.pythonhosted.org/packages/95/d2/f587cb965a56e992634bebc8611c5b579af912b74e04eb9164bd49527d21/mcp-1.6.0.tar.gz", hash = "sha256:d9324876de2c5637369f43161cd71eebfd803df5a95e46225cab8d280e366723", size = 200031 } 167 | wheels = [ 168 | { url = "https://files.pythonhosted.org/packages/10/30/20a7f33b0b884a9d14dd3aa94ff1ac9da1479fe2ad66dd9e2736075d2506/mcp-1.6.0-py3-none-any.whl", hash = "sha256:7bd24c6ea042dbec44c754f100984d186620d8b841ec30f1b19eda9b93a634d0", size = 76077 }, 169 | ] 170 | 171 | [package.optional-dependencies] 172 | cli = [ 173 | { name = "python-dotenv" }, 174 | { name = "typer" }, 175 | ] 176 | 177 | [[package]] 178 | name = "mdurl" 179 | version = "0.1.2" 180 | source = { registry = "https://pypi.org/simple" } 181 | sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } 182 | wheels = [ 183 | { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, 184 | ] 185 | 186 | [[package]] 187 | name = "pydantic" 188 | version = "2.11.3" 189 | source = { registry = "https://pypi.org/simple" } 190 | dependencies = [ 191 | { name = "annotated-types" }, 192 | { name = "pydantic-core" }, 193 | { name = "typing-extensions" }, 194 | { name = "typing-inspection" }, 195 | ] 196 | sdist = { url = "https://files.pythonhosted.org/packages/10/2e/ca897f093ee6c5f3b0bee123ee4465c50e75431c3d5b6a3b44a47134e891/pydantic-2.11.3.tar.gz", hash = "sha256:7471657138c16adad9322fe3070c0116dd6c3ad8d649300e3cbdfe91f4db4ec3", size = 785513 } 197 | wheels = [ 198 | { url = "https://files.pythonhosted.org/packages/b0/1d/407b29780a289868ed696d1616f4aad49d6388e5a77f567dcd2629dcd7b8/pydantic-2.11.3-py3-none-any.whl", hash = "sha256:a082753436a07f9ba1289c6ffa01cd93db3548776088aa917cc43b63f68fa60f", size = 443591 }, 199 | ] 200 | 201 | [[package]] 202 | name = "pydantic-core" 203 | version = "2.33.1" 204 | source = { registry = "https://pypi.org/simple" } 205 | dependencies = [ 206 | { name = "typing-extensions" }, 207 | ] 208 | sdist = { url = "https://files.pythonhosted.org/packages/17/19/ed6a078a5287aea7922de6841ef4c06157931622c89c2a47940837b5eecd/pydantic_core-2.33.1.tar.gz", hash = "sha256:bcc9c6fdb0ced789245b02b7d6603e17d1563064ddcfc36f046b61c0c05dd9df", size = 434395 } 209 | wheels = [ 210 | { url = "https://files.pythonhosted.org/packages/38/ea/5f572806ab4d4223d11551af814d243b0e3e02cc6913def4d1fe4a5ca41c/pydantic_core-2.33.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3077cfdb6125cc8dab61b155fdd714663e401f0e6883f9632118ec12cf42df26", size = 2044021 }, 211 | { url = "https://files.pythonhosted.org/packages/8c/d1/f86cc96d2aa80e3881140d16d12ef2b491223f90b28b9a911346c04ac359/pydantic_core-2.33.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8ffab8b2908d152e74862d276cf5017c81a2f3719f14e8e3e8d6b83fda863927", size = 1861742 }, 212 | { url = "https://files.pythonhosted.org/packages/37/08/fbd2cd1e9fc735a0df0142fac41c114ad9602d1c004aea340169ae90973b/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5183e4f6a2d468787243ebcd70cf4098c247e60d73fb7d68d5bc1e1beaa0c4db", size = 1910414 }, 213 | { url = "https://files.pythonhosted.org/packages/7f/73/3ac217751decbf8d6cb9443cec9b9eb0130eeada6ae56403e11b486e277e/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:398a38d323f37714023be1e0285765f0a27243a8b1506b7b7de87b647b517e48", size = 1996848 }, 214 | { url = "https://files.pythonhosted.org/packages/9a/f5/5c26b265cdcff2661e2520d2d1e9db72d117ea00eb41e00a76efe68cb009/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87d3776f0001b43acebfa86f8c64019c043b55cc5a6a2e313d728b5c95b46969", size = 2141055 }, 215 | { url = "https://files.pythonhosted.org/packages/5d/14/a9c3cee817ef2f8347c5ce0713e91867a0dceceefcb2973942855c917379/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c566dd9c5f63d22226409553531f89de0cac55397f2ab8d97d6f06cfce6d947e", size = 2753806 }, 216 | { url = "https://files.pythonhosted.org/packages/f2/68/866ce83a51dd37e7c604ce0050ff6ad26de65a7799df89f4db87dd93d1d6/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0d5f3acc81452c56895e90643a625302bd6be351e7010664151cc55b7b97f89", size = 2007777 }, 217 | { url = "https://files.pythonhosted.org/packages/b6/a8/36771f4404bb3e49bd6d4344da4dede0bf89cc1e01f3b723c47248a3761c/pydantic_core-2.33.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d3a07fadec2a13274a8d861d3d37c61e97a816beae717efccaa4b36dfcaadcde", size = 2122803 }, 218 | { url = "https://files.pythonhosted.org/packages/18/9c/730a09b2694aa89360d20756369822d98dc2f31b717c21df33b64ffd1f50/pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f99aeda58dce827f76963ee87a0ebe75e648c72ff9ba1174a253f6744f518f65", size = 2086755 }, 219 | { url = "https://files.pythonhosted.org/packages/54/8e/2dccd89602b5ec31d1c58138d02340ecb2ebb8c2cac3cc66b65ce3edb6ce/pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:902dbc832141aa0ec374f4310f1e4e7febeebc3256f00dc359a9ac3f264a45dc", size = 2257358 }, 220 | { url = "https://files.pythonhosted.org/packages/d1/9c/126e4ac1bfad8a95a9837acdd0963695d69264179ba4ede8b8c40d741702/pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fe44d56aa0b00d66640aa84a3cbe80b7a3ccdc6f0b1ca71090696a6d4777c091", size = 2257916 }, 221 | { url = "https://files.pythonhosted.org/packages/7d/ba/91eea2047e681a6853c81c20aeca9dcdaa5402ccb7404a2097c2adf9d038/pydantic_core-2.33.1-cp310-cp310-win32.whl", hash = "sha256:ed3eb16d51257c763539bde21e011092f127a2202692afaeaccb50db55a31383", size = 1923823 }, 222 | { url = "https://files.pythonhosted.org/packages/94/c0/fcdf739bf60d836a38811476f6ecd50374880b01e3014318b6e809ddfd52/pydantic_core-2.33.1-cp310-cp310-win_amd64.whl", hash = "sha256:694ad99a7f6718c1a498dc170ca430687a39894a60327f548e02a9c7ee4b6504", size = 1952494 }, 223 | { url = "https://files.pythonhosted.org/packages/d6/7f/c6298830cb780c46b4f46bb24298d01019ffa4d21769f39b908cd14bbd50/pydantic_core-2.33.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e966fc3caaf9f1d96b349b0341c70c8d6573bf1bac7261f7b0ba88f96c56c24", size = 2044224 }, 224 | { url = "https://files.pythonhosted.org/packages/a8/65/6ab3a536776cad5343f625245bd38165d6663256ad43f3a200e5936afd6c/pydantic_core-2.33.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bfd0adeee563d59c598ceabddf2c92eec77abcb3f4a391b19aa7366170bd9e30", size = 1858845 }, 225 | { url = "https://files.pythonhosted.org/packages/e9/15/9a22fd26ba5ee8c669d4b8c9c244238e940cd5d818649603ca81d1c69861/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91815221101ad3c6b507804178a7bb5cb7b2ead9ecd600041669c8d805ebd595", size = 1910029 }, 226 | { url = "https://files.pythonhosted.org/packages/d5/33/8cb1a62818974045086f55f604044bf35b9342900318f9a2a029a1bec460/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9fea9c1869bb4742d174a57b4700c6dadea951df8b06de40c2fedb4f02931c2e", size = 1997784 }, 227 | { url = "https://files.pythonhosted.org/packages/c0/ca/49958e4df7715c71773e1ea5be1c74544923d10319173264e6db122543f9/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d20eb4861329bb2484c021b9d9a977566ab16d84000a57e28061151c62b349a", size = 2141075 }, 228 | { url = "https://files.pythonhosted.org/packages/7b/a6/0b3a167a9773c79ba834b959b4e18c3ae9216b8319bd8422792abc8a41b1/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb935c5591573ae3201640579f30128ccc10739b45663f93c06796854405505", size = 2745849 }, 229 | { url = "https://files.pythonhosted.org/packages/0b/60/516484135173aa9e5861d7a0663dce82e4746d2e7f803627d8c25dfa5578/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c964fd24e6166420d18fb53996d8c9fd6eac9bf5ae3ec3d03015be4414ce497f", size = 2005794 }, 230 | { url = "https://files.pythonhosted.org/packages/86/70/05b1eb77459ad47de00cf78ee003016da0cedf8b9170260488d7c21e9181/pydantic_core-2.33.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:681d65e9011f7392db5aa002b7423cc442d6a673c635668c227c6c8d0e5a4f77", size = 2123237 }, 231 | { url = "https://files.pythonhosted.org/packages/c7/57/12667a1409c04ae7dc95d3b43158948eb0368e9c790be8b095cb60611459/pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e100c52f7355a48413e2999bfb4e139d2977a904495441b374f3d4fb4a170961", size = 2086351 }, 232 | { url = "https://files.pythonhosted.org/packages/57/61/cc6d1d1c1664b58fdd6ecc64c84366c34ec9b606aeb66cafab6f4088974c/pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:048831bd363490be79acdd3232f74a0e9951b11b2b4cc058aeb72b22fdc3abe1", size = 2258914 }, 233 | { url = "https://files.pythonhosted.org/packages/d1/0a/edb137176a1f5419b2ddee8bde6a0a548cfa3c74f657f63e56232df8de88/pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bdc84017d28459c00db6f918a7272a5190bec3090058334e43a76afb279eac7c", size = 2257385 }, 234 | { url = "https://files.pythonhosted.org/packages/26/3c/48ca982d50e4b0e1d9954919c887bdc1c2b462801bf408613ccc641b3daa/pydantic_core-2.33.1-cp311-cp311-win32.whl", hash = "sha256:32cd11c5914d1179df70406427097c7dcde19fddf1418c787540f4b730289896", size = 1923765 }, 235 | { url = "https://files.pythonhosted.org/packages/33/cd/7ab70b99e5e21559f5de38a0928ea84e6f23fdef2b0d16a6feaf942b003c/pydantic_core-2.33.1-cp311-cp311-win_amd64.whl", hash = "sha256:2ea62419ba8c397e7da28a9170a16219d310d2cf4970dbc65c32faf20d828c83", size = 1950688 }, 236 | { url = "https://files.pythonhosted.org/packages/4b/ae/db1fc237b82e2cacd379f63e3335748ab88b5adde98bf7544a1b1bd10a84/pydantic_core-2.33.1-cp311-cp311-win_arm64.whl", hash = "sha256:fc903512177361e868bc1f5b80ac8c8a6e05fcdd574a5fb5ffeac5a9982b9e89", size = 1908185 }, 237 | { url = "https://files.pythonhosted.org/packages/c8/ce/3cb22b07c29938f97ff5f5bb27521f95e2ebec399b882392deb68d6c440e/pydantic_core-2.33.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1293d7febb995e9d3ec3ea09caf1a26214eec45b0f29f6074abb004723fc1de8", size = 2026640 }, 238 | { url = "https://files.pythonhosted.org/packages/19/78/f381d643b12378fee782a72126ec5d793081ef03791c28a0fd542a5bee64/pydantic_core-2.33.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:99b56acd433386c8f20be5c4000786d1e7ca0523c8eefc995d14d79c7a081498", size = 1852649 }, 239 | { url = "https://files.pythonhosted.org/packages/9d/2b/98a37b80b15aac9eb2c6cfc6dbd35e5058a352891c5cce3a8472d77665a6/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35a5ec3fa8c2fe6c53e1b2ccc2454398f95d5393ab398478f53e1afbbeb4d939", size = 1892472 }, 240 | { url = "https://files.pythonhosted.org/packages/4e/d4/3c59514e0f55a161004792b9ff3039da52448f43f5834f905abef9db6e4a/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b172f7b9d2f3abc0efd12e3386f7e48b576ef309544ac3a63e5e9cdd2e24585d", size = 1977509 }, 241 | { url = "https://files.pythonhosted.org/packages/a9/b6/c2c7946ef70576f79a25db59a576bce088bdc5952d1b93c9789b091df716/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9097b9f17f91eea659b9ec58148c0747ec354a42f7389b9d50701610d86f812e", size = 2128702 }, 242 | { url = "https://files.pythonhosted.org/packages/88/fe/65a880f81e3f2a974312b61f82a03d85528f89a010ce21ad92f109d94deb/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc77ec5b7e2118b152b0d886c7514a4653bcb58c6b1d760134a9fab915f777b3", size = 2679428 }, 243 | { url = "https://files.pythonhosted.org/packages/6f/ff/4459e4146afd0462fb483bb98aa2436d69c484737feaceba1341615fb0ac/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3d15245b08fa4a84cefc6c9222e6f37c98111c8679fbd94aa145f9a0ae23d", size = 2008753 }, 244 | { url = "https://files.pythonhosted.org/packages/7c/76/1c42e384e8d78452ededac8b583fe2550c84abfef83a0552e0e7478ccbc3/pydantic_core-2.33.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef99779001d7ac2e2461d8ab55d3373fe7315caefdbecd8ced75304ae5a6fc6b", size = 2114849 }, 245 | { url = "https://files.pythonhosted.org/packages/00/72/7d0cf05095c15f7ffe0eb78914b166d591c0eed72f294da68378da205101/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fc6bf8869e193855e8d91d91f6bf59699a5cdfaa47a404e278e776dd7f168b39", size = 2069541 }, 246 | { url = "https://files.pythonhosted.org/packages/b3/69/94a514066bb7d8be499aa764926937409d2389c09be0b5107a970286ef81/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:b1caa0bc2741b043db7823843e1bde8aaa58a55a58fda06083b0569f8b45693a", size = 2239225 }, 247 | { url = "https://files.pythonhosted.org/packages/84/b0/e390071eadb44b41f4f54c3cef64d8bf5f9612c92686c9299eaa09e267e2/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ec259f62538e8bf364903a7d0d0239447059f9434b284f5536e8402b7dd198db", size = 2248373 }, 248 | { url = "https://files.pythonhosted.org/packages/d6/b2/288b3579ffc07e92af66e2f1a11be3b056fe1214aab314748461f21a31c3/pydantic_core-2.33.1-cp312-cp312-win32.whl", hash = "sha256:e14f369c98a7c15772b9da98987f58e2b509a93235582838bd0d1d8c08b68fda", size = 1907034 }, 249 | { url = "https://files.pythonhosted.org/packages/02/28/58442ad1c22b5b6742b992ba9518420235adced665513868f99a1c2638a5/pydantic_core-2.33.1-cp312-cp312-win_amd64.whl", hash = "sha256:1c607801d85e2e123357b3893f82c97a42856192997b95b4d8325deb1cd0c5f4", size = 1956848 }, 250 | { url = "https://files.pythonhosted.org/packages/a1/eb/f54809b51c7e2a1d9f439f158b8dd94359321abcc98767e16fc48ae5a77e/pydantic_core-2.33.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d13f0276806ee722e70a1c93da19748594f19ac4299c7e41237fc791d1861ea", size = 1903986 }, 251 | { url = "https://files.pythonhosted.org/packages/7a/24/eed3466a4308d79155f1cdd5c7432c80ddcc4530ba8623b79d5ced021641/pydantic_core-2.33.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:70af6a21237b53d1fe7b9325b20e65cbf2f0a848cf77bed492b029139701e66a", size = 2033551 }, 252 | { url = "https://files.pythonhosted.org/packages/ab/14/df54b1a0bc9b6ded9b758b73139d2c11b4e8eb43e8ab9c5847c0a2913ada/pydantic_core-2.33.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:282b3fe1bbbe5ae35224a0dbd05aed9ccabccd241e8e6b60370484234b456266", size = 1852785 }, 253 | { url = "https://files.pythonhosted.org/packages/fa/96/e275f15ff3d34bb04b0125d9bc8848bf69f25d784d92a63676112451bfb9/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b315e596282bbb5822d0c7ee9d255595bd7506d1cb20c2911a4da0b970187d3", size = 1897758 }, 254 | { url = "https://files.pythonhosted.org/packages/b7/d8/96bc536e975b69e3a924b507d2a19aedbf50b24e08c80fb00e35f9baaed8/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1dfae24cf9921875ca0ca6a8ecb4bb2f13c855794ed0d468d6abbec6e6dcd44a", size = 1986109 }, 255 | { url = "https://files.pythonhosted.org/packages/90/72/ab58e43ce7e900b88cb571ed057b2fcd0e95b708a2e0bed475b10130393e/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6dd8ecfde08d8bfadaea669e83c63939af76f4cf5538a72597016edfa3fad516", size = 2129159 }, 256 | { url = "https://files.pythonhosted.org/packages/dc/3f/52d85781406886c6870ac995ec0ba7ccc028b530b0798c9080531b409fdb/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f593494876eae852dc98c43c6f260f45abdbfeec9e4324e31a481d948214764", size = 2680222 }, 257 | { url = "https://files.pythonhosted.org/packages/f4/56/6e2ef42f363a0eec0fd92f74a91e0ac48cd2e49b695aac1509ad81eee86a/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:948b73114f47fd7016088e5186d13faf5e1b2fe83f5e320e371f035557fd264d", size = 2006980 }, 258 | { url = "https://files.pythonhosted.org/packages/4c/c0/604536c4379cc78359f9ee0aa319f4aedf6b652ec2854953f5a14fc38c5a/pydantic_core-2.33.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e11f3864eb516af21b01e25fac915a82e9ddad3bb0fb9e95a246067398b435a4", size = 2120840 }, 259 | { url = "https://files.pythonhosted.org/packages/1f/46/9eb764814f508f0edfb291a0f75d10854d78113fa13900ce13729aaec3ae/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:549150be302428b56fdad0c23c2741dcdb5572413776826c965619a25d9c6bde", size = 2072518 }, 260 | { url = "https://files.pythonhosted.org/packages/42/e3/fb6b2a732b82d1666fa6bf53e3627867ea3131c5f39f98ce92141e3e3dc1/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:495bc156026efafd9ef2d82372bd38afce78ddd82bf28ef5276c469e57c0c83e", size = 2248025 }, 261 | { url = "https://files.pythonhosted.org/packages/5c/9d/fbe8fe9d1aa4dac88723f10a921bc7418bd3378a567cb5e21193a3c48b43/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ec79de2a8680b1a67a07490bddf9636d5c2fab609ba8c57597e855fa5fa4dacd", size = 2254991 }, 262 | { url = "https://files.pythonhosted.org/packages/aa/99/07e2237b8a66438d9b26482332cda99a9acccb58d284af7bc7c946a42fd3/pydantic_core-2.33.1-cp313-cp313-win32.whl", hash = "sha256:ee12a7be1742f81b8a65b36c6921022301d466b82d80315d215c4c691724986f", size = 1915262 }, 263 | { url = "https://files.pythonhosted.org/packages/8a/f4/e457a7849beeed1e5defbcf5051c6f7b3c91a0624dd31543a64fc9adcf52/pydantic_core-2.33.1-cp313-cp313-win_amd64.whl", hash = "sha256:ede9b407e39949d2afc46385ce6bd6e11588660c26f80576c11c958e6647bc40", size = 1956626 }, 264 | { url = "https://files.pythonhosted.org/packages/20/d0/e8d567a7cff7b04e017ae164d98011f1e1894269fe8e90ea187a3cbfb562/pydantic_core-2.33.1-cp313-cp313-win_arm64.whl", hash = "sha256:aa687a23d4b7871a00e03ca96a09cad0f28f443690d300500603bd0adba4b523", size = 1909590 }, 265 | { url = "https://files.pythonhosted.org/packages/ef/fd/24ea4302d7a527d672c5be06e17df16aabfb4e9fdc6e0b345c21580f3d2a/pydantic_core-2.33.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:401d7b76e1000d0dd5538e6381d28febdcacb097c8d340dde7d7fc6e13e9f95d", size = 1812963 }, 266 | { url = "https://files.pythonhosted.org/packages/5f/95/4fbc2ecdeb5c1c53f1175a32d870250194eb2fdf6291b795ab08c8646d5d/pydantic_core-2.33.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7aeb055a42d734c0255c9e489ac67e75397d59c6fbe60d155851e9782f276a9c", size = 1986896 }, 267 | { url = "https://files.pythonhosted.org/packages/71/ae/fe31e7f4a62431222d8f65a3bd02e3fa7e6026d154a00818e6d30520ea77/pydantic_core-2.33.1-cp313-cp313t-win_amd64.whl", hash = "sha256:338ea9b73e6e109f15ab439e62cb3b78aa752c7fd9536794112e14bee02c8d18", size = 1931810 }, 268 | { url = "https://files.pythonhosted.org/packages/9c/c7/8b311d5adb0fe00a93ee9b4e92a02b0ec08510e9838885ef781ccbb20604/pydantic_core-2.33.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c834f54f8f4640fd7e4b193f80eb25a0602bba9e19b3cd2fc7ffe8199f5ae02", size = 2041659 }, 269 | { url = "https://files.pythonhosted.org/packages/8a/d6/4f58d32066a9e26530daaf9adc6664b01875ae0691570094968aaa7b8fcc/pydantic_core-2.33.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:049e0de24cf23766f12cc5cc71d8abc07d4a9deb9061b334b62093dedc7cb068", size = 1873294 }, 270 | { url = "https://files.pythonhosted.org/packages/f7/3f/53cc9c45d9229da427909c751f8ed2bf422414f7664ea4dde2d004f596ba/pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a28239037b3d6f16916a4c831a5a0eadf856bdd6d2e92c10a0da3a59eadcf3e", size = 1903771 }, 271 | { url = "https://files.pythonhosted.org/packages/f0/49/bf0783279ce674eb9903fb9ae43f6c614cb2f1c4951370258823f795368b/pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d3da303ab5f378a268fa7d45f37d7d85c3ec19769f28d2cc0c61826a8de21fe", size = 2083558 }, 272 | { url = "https://files.pythonhosted.org/packages/9c/5b/0d998367687f986c7d8484a2c476d30f07bf5b8b1477649a6092bd4c540e/pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:25626fb37b3c543818c14821afe0fd3830bc327a43953bc88db924b68c5723f1", size = 2118038 }, 273 | { url = "https://files.pythonhosted.org/packages/b3/33/039287d410230ee125daee57373ac01940d3030d18dba1c29cd3089dc3ca/pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3ab2d36e20fbfcce8f02d73c33a8a7362980cff717926bbae030b93ae46b56c7", size = 2079315 }, 274 | { url = "https://files.pythonhosted.org/packages/1f/85/6d8b2646d99c062d7da2d0ab2faeb0d6ca9cca4c02da6076376042a20da3/pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2f9284e11c751b003fd4215ad92d325d92c9cb19ee6729ebd87e3250072cdcde", size = 2249063 }, 275 | { url = "https://files.pythonhosted.org/packages/17/d7/c37d208d5738f7b9ad8f22ae8a727d88ebf9c16c04ed2475122cc3f7224a/pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:048c01eee07d37cbd066fc512b9d8b5ea88ceeb4e629ab94b3e56965ad655add", size = 2254631 }, 276 | { url = "https://files.pythonhosted.org/packages/13/e0/bafa46476d328e4553b85ab9b2f7409e7aaef0ce4c937c894821c542d347/pydantic_core-2.33.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5ccd429694cf26af7997595d627dd2637e7932214486f55b8a357edaac9dae8c", size = 2080877 }, 277 | { url = "https://files.pythonhosted.org/packages/0b/76/1794e440c1801ed35415238d2c728f26cd12695df9057154ad768b7b991c/pydantic_core-2.33.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3a371dc00282c4b84246509a5ddc808e61b9864aa1eae9ecc92bb1268b82db4a", size = 2042858 }, 278 | { url = "https://files.pythonhosted.org/packages/73/b4/9cd7b081fb0b1b4f8150507cd59d27b275c3e22ad60b35cb19ea0977d9b9/pydantic_core-2.33.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f59295ecc75a1788af8ba92f2e8c6eeaa5a94c22fc4d151e8d9638814f85c8fc", size = 1873745 }, 279 | { url = "https://files.pythonhosted.org/packages/e1/d7/9ddb7575d4321e40d0363903c2576c8c0c3280ebea137777e5ab58d723e3/pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08530b8ac922003033f399128505f513e30ca770527cc8bbacf75a84fcc2c74b", size = 1904188 }, 280 | { url = "https://files.pythonhosted.org/packages/d1/a8/3194ccfe461bb08da19377ebec8cb4f13c9bd82e13baebc53c5c7c39a029/pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae370459da6a5466978c0eacf90690cb57ec9d533f8e63e564ef3822bfa04fe", size = 2083479 }, 281 | { url = "https://files.pythonhosted.org/packages/42/c7/84cb569555d7179ca0b3f838cef08f66f7089b54432f5b8599aac6e9533e/pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e3de2777e3b9f4d603112f78006f4ae0acb936e95f06da6cb1a45fbad6bdb4b5", size = 2118415 }, 282 | { url = "https://files.pythonhosted.org/packages/3b/67/72abb8c73e0837716afbb58a59cc9e3ae43d1aa8677f3b4bc72c16142716/pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3a64e81e8cba118e108d7126362ea30e021291b7805d47e4896e52c791be2761", size = 2079623 }, 283 | { url = "https://files.pythonhosted.org/packages/0b/cd/c59707e35a47ba4cbbf153c3f7c56420c58653b5801b055dc52cccc8e2dc/pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:52928d8c1b6bda03cc6d811e8923dffc87a2d3c8b3bfd2ce16471c7147a24850", size = 2250175 }, 284 | { url = "https://files.pythonhosted.org/packages/84/32/e4325a6676b0bed32d5b084566ec86ed7fd1e9bcbfc49c578b1755bde920/pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:1b30d92c9412beb5ac6b10a3eb7ef92ccb14e3f2a8d7732e2d739f58b3aa7544", size = 2254674 }, 285 | { url = "https://files.pythonhosted.org/packages/12/6f/5596dc418f2e292ffc661d21931ab34591952e2843e7168ea5a52591f6ff/pydantic_core-2.33.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f995719707e0e29f0f41a8aa3bcea6e761a36c9136104d3189eafb83f5cec5e5", size = 2080951 }, 286 | ] 287 | 288 | [[package]] 289 | name = "pydantic-settings" 290 | version = "2.8.1" 291 | source = { registry = "https://pypi.org/simple" } 292 | dependencies = [ 293 | { name = "pydantic" }, 294 | { name = "python-dotenv" }, 295 | ] 296 | sdist = { url = "https://files.pythonhosted.org/packages/88/82/c79424d7d8c29b994fb01d277da57b0a9b09cc03c3ff875f9bd8a86b2145/pydantic_settings-2.8.1.tar.gz", hash = "sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585", size = 83550 } 297 | wheels = [ 298 | { url = "https://files.pythonhosted.org/packages/0b/53/a64f03044927dc47aafe029c42a5b7aabc38dfb813475e0e1bf71c4a59d0/pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c", size = 30839 }, 299 | ] 300 | 301 | [[package]] 302 | name = "pygments" 303 | version = "2.19.1" 304 | source = { registry = "https://pypi.org/simple" } 305 | sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } 306 | wheels = [ 307 | { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, 308 | ] 309 | 310 | [[package]] 311 | name = "python-dotenv" 312 | version = "1.1.0" 313 | source = { registry = "https://pypi.org/simple" } 314 | sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920 } 315 | wheels = [ 316 | { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256 }, 317 | ] 318 | 319 | [[package]] 320 | name = "rich" 321 | version = "14.0.0" 322 | source = { registry = "https://pypi.org/simple" } 323 | dependencies = [ 324 | { name = "markdown-it-py" }, 325 | { name = "pygments" }, 326 | { name = "typing-extensions", marker = "python_full_version < '3.11'" }, 327 | ] 328 | sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078 } 329 | wheels = [ 330 | { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229 }, 331 | ] 332 | 333 | [[package]] 334 | name = "shellingham" 335 | version = "1.5.4" 336 | source = { registry = "https://pypi.org/simple" } 337 | sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 } 338 | wheels = [ 339 | { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, 340 | ] 341 | 342 | [[package]] 343 | name = "sniffio" 344 | version = "1.3.1" 345 | source = { registry = "https://pypi.org/simple" } 346 | sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } 347 | wheels = [ 348 | { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, 349 | ] 350 | 351 | [[package]] 352 | name = "sse-starlette" 353 | version = "2.2.1" 354 | source = { registry = "https://pypi.org/simple" } 355 | dependencies = [ 356 | { name = "anyio" }, 357 | { name = "starlette" }, 358 | ] 359 | sdist = { url = "https://files.pythonhosted.org/packages/71/a4/80d2a11af59fe75b48230846989e93979c892d3a20016b42bb44edb9e398/sse_starlette-2.2.1.tar.gz", hash = "sha256:54470d5f19274aeed6b2d473430b08b4b379ea851d953b11d7f1c4a2c118b419", size = 17376 } 360 | wheels = [ 361 | { url = "https://files.pythonhosted.org/packages/d9/e0/5b8bd393f27f4a62461c5cf2479c75a2cc2ffa330976f9f00f5f6e4f50eb/sse_starlette-2.2.1-py3-none-any.whl", hash = "sha256:6410a3d3ba0c89e7675d4c273a301d64649c03a5ef1ca101f10b47f895fd0e99", size = 10120 }, 362 | ] 363 | 364 | [[package]] 365 | name = "starlette" 366 | version = "0.46.2" 367 | source = { registry = "https://pypi.org/simple" } 368 | dependencies = [ 369 | { name = "anyio" }, 370 | ] 371 | sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846 } 372 | wheels = [ 373 | { url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037 }, 374 | ] 375 | 376 | [[package]] 377 | name = "typer" 378 | version = "0.15.2" 379 | source = { registry = "https://pypi.org/simple" } 380 | dependencies = [ 381 | { name = "click" }, 382 | { name = "rich" }, 383 | { name = "shellingham" }, 384 | { name = "typing-extensions" }, 385 | ] 386 | sdist = { url = "https://files.pythonhosted.org/packages/8b/6f/3991f0f1c7fcb2df31aef28e0594d8d54b05393a0e4e34c65e475c2a5d41/typer-0.15.2.tar.gz", hash = "sha256:ab2fab47533a813c49fe1f16b1a370fd5819099c00b119e0633df65f22144ba5", size = 100711 } 387 | wheels = [ 388 | { url = "https://files.pythonhosted.org/packages/7f/fc/5b29fea8cee020515ca82cc68e3b8e1e34bb19a3535ad854cac9257b414c/typer-0.15.2-py3-none-any.whl", hash = "sha256:46a499c6107d645a9c13f7ee46c5d5096cae6f5fc57dd11eccbbb9ae3e44ddfc", size = 45061 }, 389 | ] 390 | 391 | [[package]] 392 | name = "typing-extensions" 393 | version = "4.13.2" 394 | source = { registry = "https://pypi.org/simple" } 395 | sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967 } 396 | wheels = [ 397 | { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806 }, 398 | ] 399 | 400 | [[package]] 401 | name = "typing-inspection" 402 | version = "0.4.0" 403 | source = { registry = "https://pypi.org/simple" } 404 | dependencies = [ 405 | { name = "typing-extensions" }, 406 | ] 407 | sdist = { url = "https://files.pythonhosted.org/packages/82/5c/e6082df02e215b846b4b8c0b887a64d7d08ffaba30605502639d44c06b82/typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122", size = 76222 } 408 | wheels = [ 409 | { url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125 }, 410 | ] 411 | 412 | [[package]] 413 | name = "uvicorn" 414 | version = "0.34.1" 415 | source = { registry = "https://pypi.org/simple" } 416 | dependencies = [ 417 | { name = "click" }, 418 | { name = "h11" }, 419 | { name = "typing-extensions", marker = "python_full_version < '3.11'" }, 420 | ] 421 | sdist = { url = "https://files.pythonhosted.org/packages/86/37/dd92f1f9cedb5eaf74d9999044306e06abe65344ff197864175dbbd91871/uvicorn-0.34.1.tar.gz", hash = "sha256:af981725fc4b7ffc5cb3b0e9eda6258a90c4b52cb2a83ce567ae0a7ae1757afc", size = 76755 } 422 | wheels = [ 423 | { url = "https://files.pythonhosted.org/packages/5f/38/a5801450940a858c102a7ad9e6150146a25406a119851c993148d56ab041/uvicorn-0.34.1-py3-none-any.whl", hash = "sha256:984c3a8c7ca18ebaad15995ee7401179212c59521e67bfc390c07fa2b8d2e065", size = 62404 }, 424 | ] 425 | -------------------------------------------------------------------------------- /dart.py: -------------------------------------------------------------------------------- 1 | import httpx 2 | from typing import Any, Dict, List, Optional, Tuple, Set 3 | from mcp.server.fastmcp import FastMCP, Context 4 | import os 5 | import zipfile 6 | import xml.etree.ElementTree as ET 7 | from io import BytesIO, StringIO 8 | import re 9 | import traceback 10 | from datetime import datetime, timedelta 11 | from dotenv import load_dotenv 12 | 13 | 14 | load_dotenv() 15 | # 상수 정의 16 | # API 설정 17 | API_KEY = os.environ.get("DART_API_KEY") # 환경 변수에서 API 키 로드, 없으면 기본값 사용 18 | BASE_URL = "https://opendart.fss.or.kr/api" 19 | 20 | # 보고서 코드 21 | REPORT_CODE = { 22 | "사업보고서": "11011", 23 | "반기보고서": "11012", 24 | "1분기보고서": "11013", 25 | "3분기보고서": "11014" 26 | } 27 | 28 | # 재무상태표 항목 리스트 - 확장 29 | BALANCE_SHEET_ITEMS = [ 30 | "유동자산", "비유동자산", "자산총계", 31 | "유동부채", "비유동부채", "부채총계", 32 | "자본금", "자본잉여금", "이익잉여금", "기타자본항목", "자본총계" 33 | ] 34 | 35 | # 현금흐름표 항목 리스트 36 | CASH_FLOW_ITEMS = ["영업활동 현금흐름", "투자활동 현금흐름", "재무활동 현금흐름"] 37 | 38 | # 보고서 유형별 contextRef 패턴 정의 39 | REPORT_PATTERNS = { 40 | "연간": "FY", 41 | "3분기": "TQQ", # 손익계산서는 TQQ 42 | "반기": "HYA", 43 | "1분기": "FQA" 44 | } 45 | 46 | # 현금흐름표용 특별 패턴 47 | CASH_FLOW_PATTERNS = { 48 | "연간": "FY", 49 | "3분기": "TQA", # 현금흐름표는 TQA 50 | "반기": "HYA", 51 | "1분기": "FQA" 52 | } 53 | 54 | # 재무상태표용 특별 패턴 55 | BALANCE_SHEET_PATTERNS = { 56 | "연간": "FY", 57 | "3분기": "TQA", # 재무상태표도 TQA 58 | "반기": "HYA", 59 | "1분기": "FQA" 60 | } 61 | 62 | # 데이터 무효/오류 상태 표시자 63 | INVALID_VALUE_INDICATORS = {"N/A", "XBRL 파싱 오류", "데이터 추출 오류"} 64 | 65 | # MCP 서버 초기화 66 | mcp = FastMCP("dart") 67 | 68 | # 재무제표 유형 정의 69 | STATEMENT_TYPES = { 70 | "재무상태표": "BS", 71 | "손익계산서": "IS", 72 | "현금흐름표": "CF" 73 | } 74 | 75 | # 세부 항목 태그 정의 76 | DETAILED_TAGS = { 77 | "재무상태표": { 78 | "유동자산": ["ifrs-full:CurrentAssets"], 79 | "비유동자산": ["ifrs-full:NoncurrentAssets"], 80 | "자산총계": ["ifrs-full:Assets"], 81 | "유동부채": ["ifrs-full:CurrentLiabilities"], 82 | "비유동부채": ["ifrs-full:NoncurrentLiabilities"], 83 | "부채총계": ["ifrs-full:Liabilities"], 84 | "자본금": ["ifrs-full:IssuedCapital"], 85 | "자본잉여금": ["ifrs-full:SharePremium"], 86 | "이익잉여금": ["ifrs-full:RetainedEarnings"], 87 | "기타자본항목": ["dart:ElementsOfOtherStockholdersEquity"], 88 | "자본총계": ["ifrs-full:Equity"] 89 | }, 90 | "손익계산서": { 91 | "매출액": ["ifrs-full:Revenue"], 92 | "매출원가": ["ifrs-full:CostOfSales"], 93 | "매출총이익": ["ifrs-full:GrossProfit"], 94 | "판매비와관리비": ["dart:TotalSellingGeneralAdministrativeExpenses"], 95 | "영업이익": ["dart:OperatingIncomeLoss"], 96 | "금융수익": ["ifrs-full:FinanceIncome"], 97 | "금융비용": ["ifrs-full:FinanceCosts"], 98 | "법인세비용차감전순이익": ["ifrs-full:ProfitLossBeforeTax"], 99 | "법인세비용": ["ifrs-full:IncomeTaxExpenseContinuingOperations"], 100 | "당기순이익": ["ifrs-full:ProfitLoss"], 101 | "기본주당이익": ["ifrs-full:BasicEarningsLossPerShare"] 102 | }, 103 | "현금흐름표": { 104 | "영업활동 현금흐름": ["ifrs-full:CashFlowsFromUsedInOperatingActivities"], 105 | "영업에서 창출된 현금": ["ifrs-full:CashFlowsFromUsedInOperations"], 106 | "이자수취": ["ifrs-full:InterestReceivedClassifiedAsOperatingActivities"], 107 | "이자지급": ["ifrs-full:InterestPaidClassifiedAsOperatingActivities"], 108 | "배당금수취": ["ifrs-full:DividendsReceivedClassifiedAsOperatingActivities"], 109 | "법인세납부": ["ifrs-full:IncomeTaxesPaidRefundClassifiedAsOperatingActivities"], 110 | "투자활동 현금흐름": ["ifrs-full:CashFlowsFromUsedInInvestingActivities"], 111 | "유형자산의 취득": ["ifrs-full:PurchaseOfPropertyPlantAndEquipmentClassifiedAsInvestingActivities"], 112 | "무형자산의 취득": ["ifrs-full:PurchaseOfIntangibleAssetsClassifiedAsInvestingActivities"], 113 | "유형자산의 처분": ["ifrs-full:ProceedsFromSalesOfPropertyPlantAndEquipmentClassifiedAsInvestingActivities"], 114 | "재무활동 현금흐름": ["ifrs-full:CashFlowsFromUsedInFinancingActivities"], 115 | "배당금지급": ["ifrs-full:DividendsPaidClassifiedAsFinancingActivities"], 116 | "현금및현금성자산의순증가": ["ifrs-full:IncreaseDecreaseInCashAndCashEquivalents"], 117 | "기초현금및현금성자산": ["dart:CashAndCashEquivalentsAtBeginningOfPeriodCf"], 118 | "기말현금및현금성자산": ["dart:CashAndCashEquivalentsAtEndOfPeriodCf"] 119 | } 120 | } 121 | 122 | chat_guideline = "\n* 제공된 공시정보들은 분기, 반기, 연간이 섞여있을 수 있습니다. \n사용자가 특별히 연간이나 반기데이터만을 원하는게 아니라면, 주어진 데이터를 적당히 가공하여 분기별로 사용자에게 제공하세요." ; 123 | 124 | 125 | # Helper 함수 126 | 127 | async def get_corp_code_by_name(corp_name: str) -> Tuple[str, str]: 128 | """ 129 | 회사명으로 회사의 고유번호를 검색하는 함수 130 | 131 | Args: 132 | corp_name: 검색할 회사명 133 | 134 | Returns: 135 | (고유번호, 기업이름) 튜플, 찾지 못한 경우 ("", "") 136 | """ 137 | url = f"{BASE_URL}/corpCode.xml?crtfc_key={API_KEY}" 138 | 139 | try: 140 | async with httpx.AsyncClient() as client: 141 | try: 142 | response = await client.get(url) 143 | 144 | if response.status_code != 200: 145 | return ("", f"API 요청 실패: HTTP 상태 코드 {response.status_code}") 146 | 147 | try: 148 | with zipfile.ZipFile(BytesIO(response.content)) as zip_file: 149 | try: 150 | with zip_file.open('CORPCODE.xml') as xml_file: 151 | try: 152 | tree = ET.parse(xml_file) 153 | root = tree.getroot() 154 | 155 | # 검색어를 포함하는 모든 회사 찾기 156 | matches = [] 157 | for company in root.findall('.//list'): 158 | name = company.find('corp_name').text 159 | stock_code = company.find('stock_code').text 160 | 161 | # stock_code가 비어있거나 공백만 있는 경우 건너뛰기 162 | if not stock_code or stock_code.strip() == "": 163 | continue 164 | 165 | if name and corp_name in name: 166 | # 일치도 점수 계산 (낮을수록 더 정확히 일치) 167 | score = 0 168 | if name != corp_name: 169 | score += abs(len(name) - len(corp_name)) 170 | if not name.startswith(corp_name): 171 | score += 10 172 | 173 | code = company.find('corp_code').text 174 | matches.append((name, code, score)) 175 | 176 | # 일치하는 회사가 없는 경우 177 | if not matches: 178 | return ("", f"'{corp_name}' 회사를 찾을 수 없습니다.") 179 | 180 | # 일치도 점수가 가장 낮은 (가장 일치하는) 회사 반환 181 | matches.sort(key=lambda x: x[2]) 182 | matched_name = matches[0][0] 183 | matched_code = matches[0][1] 184 | return (matched_code, matched_name) 185 | except ET.ParseError as e: 186 | return ("", f"XML 파싱 오류: {str(e)}") 187 | except Exception as e: 188 | return ("", f"ZIP 파일 내부 파일 접근 오류: {str(e)}") 189 | except zipfile.BadZipFile: 190 | return ("", "다운로드한 파일이 유효한 ZIP 파일이 아닙니다.") 191 | except Exception as e: 192 | return ("", f"ZIP 파일 처리 중 오류 발생: {str(e)}") 193 | except httpx.RequestError as e: 194 | return ("", f"API 요청 중 네트워크 오류 발생: {str(e)}") 195 | except Exception as e: 196 | return ("", f"회사 코드 조회 중 예상치 못한 오류 발생: {str(e)}") 197 | 198 | return ("", "알 수 없는 오류로 회사 정보를 찾을 수 없습니다.") 199 | 200 | 201 | async def get_disclosure_list(corp_code: str, start_date: str, end_date: str) -> Tuple[List[Dict[str, Any]], Optional[str]]: 202 | """ 203 | 기업의 정기공시 목록을 조회하는 함수 204 | 205 | Args: 206 | corp_code: 회사 고유번호(8자리) 207 | start_date: 시작일(YYYYMMDD) 208 | end_date: 종료일(YYYYMMDD) 209 | 210 | Returns: 211 | (공시 목록 리스트, 오류 메시지) 튜플. 성공 시 (목록, None), 실패 시 (빈 리스트, 오류 메시지) 212 | """ 213 | # 정기공시(A) 유형만 조회 214 | url = f"{BASE_URL}/list.json?crtfc_key={API_KEY}&corp_code={corp_code}&bgn_de={start_date}&end_de={end_date}&pblntf_ty=A&page_count=100" 215 | 216 | try: 217 | async with httpx.AsyncClient() as client: 218 | try: 219 | response = await client.get(url) 220 | 221 | if response.status_code != 200: 222 | return [], f"API 요청 실패: HTTP 상태 코드 {response.status_code}" 223 | 224 | try: 225 | result = response.json() 226 | 227 | if result.get('status') != '000': 228 | status = result.get('status', '알 수 없음') 229 | msg = result.get('message', '알 수 없는 오류') 230 | return [], f"DART API 오류: {status} - {msg}" 231 | 232 | return result.get('list', []), None 233 | except Exception as e: 234 | return [], f"응답 JSON 파싱 오류: {str(e)}" 235 | except httpx.RequestError as e: 236 | return [], f"API 요청 중 네트워크 오류 발생: {str(e)}" 237 | except Exception as e: 238 | return [], f"공시 목록 조회 중 예상치 못한 오류 발생: {str(e)}" 239 | 240 | return [], "알 수 없는 오류로 공시 목록을 조회할 수 없습니다." 241 | 242 | 243 | async def get_financial_statement_xbrl(rcept_no: str, reprt_code: str) -> str: 244 | """ 245 | 재무제표 원본파일(XBRL)을 다운로드하여 XBRL 텍스트를 반환하는 함수 246 | 247 | Args: 248 | rcept_no: 공시 접수번호(14자리) 249 | reprt_code: 보고서 코드 (11011: 사업보고서, 11012: 반기보고서, 11013: 1분기보고서, 11014: 3분기보고서) 250 | 251 | Returns: 252 | 추출된 XBRL 텍스트 내용, 실패 시 오류 메시지 문자열 253 | """ 254 | url = f"{BASE_URL}/fnlttXbrl.xml?crtfc_key={API_KEY}&rcept_no={rcept_no}&reprt_code={reprt_code}" 255 | 256 | try: 257 | async with httpx.AsyncClient(timeout=30.0) as client: 258 | response = await client.get(url) 259 | 260 | if response.status_code != 200: 261 | return f"API 요청 실패: HTTP 상태 코드 {response.status_code}" 262 | 263 | try: 264 | with zipfile.ZipFile(BytesIO(response.content)) as zip_file: 265 | xbrl_content = "" 266 | for file_name in zip_file.namelist(): 267 | if file_name.lower().endswith('.xbrl'): 268 | with zip_file.open(file_name) as xbrl_file: 269 | # XBRL 파일을 텍스트로 읽기 (UTF-8 시도, 실패 시 EUC-KR) 270 | try: 271 | xbrl_content = xbrl_file.read().decode('utf-8') 272 | except UnicodeDecodeError: 273 | try: 274 | xbrl_file.seek(0) 275 | xbrl_content = xbrl_file.read().decode('euc-kr') 276 | except UnicodeDecodeError: 277 | xbrl_content = "<인코딩 오류: XBRL 내용을 읽을 수 없습니다>" 278 | break 279 | 280 | if not xbrl_content: 281 | return "ZIP 파일 내에서 XBRL 파일을 찾을 수 없습니다." 282 | 283 | return xbrl_content 284 | 285 | except zipfile.BadZipFile: 286 | # 응답이 ZIP 파일 형식이 아닐 경우 (DART API 오류 메시지 등) 287 | try: 288 | error_content = response.content.decode('utf-8') 289 | try: 290 | root = ET.fromstring(error_content) 291 | status = root.findtext('status') 292 | message = root.findtext('message') 293 | if status and message: 294 | return f"DART API 오류: {status} - {message}" 295 | else: 296 | return f"유효하지 않은 ZIP 파일이며, 오류 메시지 파싱 실패: {error_content[:200]}" 297 | except ET.ParseError: 298 | return f"유효하지 않은 ZIP 파일이며, XML 파싱 불가: {error_content[:200]}" 299 | except Exception: 300 | return "다운로드한 파일이 유효한 ZIP 파일이 아닙니다 (내용 확인 불가)." 301 | except Exception as e: 302 | return f"ZIP 파일 처리 중 오류 발생: {str(e)}" 303 | 304 | except httpx.RequestError as e: 305 | return f"API 요청 중 네트워크 오류 발생: {str(e)}" 306 | except Exception as e: 307 | return f"XBRL 데이터 처리 중 예상치 못한 오류 발생: {str(e)}" 308 | 309 | 310 | def detect_namespaces(xbrl_content: str, base_namespaces: Dict[str, str]) -> Dict[str, str]: 311 | """ 312 | XBRL 문서에서 네임스페이스를 추출하고 기본 네임스페이스와 병합 313 | 314 | Args: 315 | xbrl_content: XBRL 문서 내용 316 | base_namespaces: 기본 네임스페이스 딕셔너리 317 | 318 | Returns: 319 | 업데이트된 네임스페이스 딕셔너리 320 | """ 321 | namespaces = base_namespaces.copy() 322 | detected = {} 323 | 324 | try: 325 | for event, node in ET.iterparse(StringIO(xbrl_content), events=['start-ns']): 326 | prefix, uri = node 327 | if prefix and prefix not in namespaces: 328 | namespaces[prefix] = uri 329 | detected[prefix] = uri 330 | elif prefix and namespaces.get(prefix) != uri: 331 | namespaces[prefix] = uri 332 | detected[prefix] = uri 333 | except Exception: 334 | pass # 네임스페이스 감지 실패 시 기본값 사용 335 | 336 | return namespaces, detected 337 | 338 | 339 | def extract_fiscal_year(context_refs: Set[str]) -> str: 340 | """ 341 | contextRef 집합에서 회계연도 추출 342 | 343 | Args: 344 | context_refs: XBRL 문서에서 추출한 contextRef 집합 345 | 346 | Returns: 347 | 감지된 회계연도 또는 현재 연도 348 | """ 349 | for context_ref in context_refs: 350 | if 'CFY' in context_ref and len(context_ref) > 7: 351 | match = re.search(r'CFY(\d{4})', context_ref) 352 | if match: 353 | return match.group(1) 354 | 355 | # 회계연도를 찾지 못한 경우, 현재 연도를 사용 356 | return str(datetime.now().year) 357 | 358 | 359 | def get_pattern_by_item_type(item_name: str) -> Dict[str, str]: 360 | """ 361 | 항목 유형에 따른 적절한 패턴 선택 362 | 363 | Args: 364 | item_name: 재무 항목 이름 365 | 366 | Returns: 367 | 항목 유형에 맞는 패턴 딕셔너리 368 | """ 369 | # 현금흐름표 항목 확인 370 | if item_name in CASH_FLOW_ITEMS or item_name in DETAILED_TAGS["현금흐름표"]: 371 | return CASH_FLOW_PATTERNS 372 | 373 | # 재무상태표 항목 확인 374 | elif item_name in BALANCE_SHEET_ITEMS or item_name in DETAILED_TAGS["재무상태표"]: 375 | return BALANCE_SHEET_PATTERNS 376 | 377 | # 손익계산서 항목 (기본값) 378 | else: 379 | return REPORT_PATTERNS 380 | 381 | 382 | def format_numeric_value(value_text: str, decimals: str) -> str: 383 | """ 384 | XBRL 숫자 값을 포맷팅 385 | 386 | Args: 387 | value_text: 숫자 텍스트 388 | decimals: 소수점 자리수 지정 (숫자 또는 "INF") 389 | 390 | Returns: 391 | 포맷팅된 숫자 문자열 392 | """ 393 | numeric_value = float(value_text.replace(',', '')) 394 | 395 | # decimals가 "INF"인 경우 원본 값 그대로 사용 396 | if decimals == "INF": 397 | if numeric_value == int(numeric_value): 398 | return f"{int(numeric_value):,}" 399 | else: 400 | return f"{numeric_value:,.2f}" 401 | 402 | # 일반적인 경우 decimals에 따라 스케일 조정 403 | numeric_value *= (10 ** -int(decimals)) 404 | 405 | if numeric_value == int(numeric_value): 406 | return f"{int(numeric_value):,}" 407 | else: 408 | return f"{numeric_value:,.2f}" 409 | 410 | 411 | def parse_xbrl_financial_data(xbrl_content: str, items_and_tags: Dict[str, List[str]]) -> Dict[str, str]: 412 | """ 413 | XBRL 텍스트 내용을 파싱하여 지정된 항목의 재무 데이터를 추출 414 | 415 | Args: 416 | xbrl_content: XBRL 파일의 전체 텍스트 내용 417 | items_and_tags: 추출할 항목과 태그 리스트 딕셔너리 418 | {'항목명': ['태그1', '태그2', ...]} 419 | 420 | Returns: 421 | 추출된 재무 데이터 딕셔너리 {'항목명': '값'} 422 | """ 423 | extracted_data = {item_name: "N/A" for item_name in items_and_tags} 424 | 425 | # 기본 네임스페이스 정의 426 | base_namespaces = { 427 | 'ifrs-full': 'http://xbrl.ifrs.org/taxonomy/2021-03-24/ifrs-full', 428 | 'dart': 'http://dart.fss.or.kr/xbrl/dte/2019-10-31', 429 | 'kor-ifrs': 'http://www.fss.or.kr/xbrl/kor/kor-ifrs/2021-03-24', 430 | } 431 | 432 | try: 433 | # XBRL 파싱 434 | root = ET.fromstring(xbrl_content) 435 | 436 | # 네임스페이스 추출 및 업데이트 437 | namespaces, detected_namespaces = detect_namespaces(xbrl_content, base_namespaces) 438 | 439 | # 모든 contextRef 값 수집 440 | all_context_refs = set() 441 | for elem in root.findall('.//*[@contextRef]'): 442 | all_context_refs.add(elem.get('contextRef')) 443 | 444 | # 회계연도 추출 445 | fiscal_year = extract_fiscal_year(all_context_refs) 446 | 447 | # 각 항목별 태그 검색 및 값 추출 448 | for item_name, tag_list in items_and_tags.items(): 449 | item_found = False 450 | 451 | for tag in tag_list: 452 | if item_found: 453 | break 454 | 455 | # 해당 태그 요소 검색 456 | elements = root.findall(f'.//{tag}', namespaces) 457 | if not elements: 458 | continue 459 | 460 | # 항목 유형에 맞는 패턴 선택 461 | patterns = get_pattern_by_item_type(item_name) 462 | 463 | # 각 보고서 유형별 패턴 시도 464 | for report_type, pattern_code in patterns.items(): 465 | if item_found: 466 | break 467 | 468 | # 기존 접두사 로직은 참조용으로만 사용 (실제 패턴 매칭에는 사용하지 않음) 469 | # 패턴에서 접두사 부분을 (.): 어떤 한 글자라도 매칭되도록 함 470 | pattern_base = f"CFY{fiscal_year}.{pattern_code}_ifrs-full_ConsolidatedAndSeparateFinancialStatementsAxis_ifrs-full_ConsolidatedMember" 471 | # 패턴의 끝에 $ 추가하여 정확히 일치하는 패턴만 매칭 472 | pattern_regex = re.compile(f"^{pattern_base}$") 473 | 474 | # 패턴과 일치하는 요소 찾기 475 | for elem in elements: 476 | context_ref = elem.get('contextRef') 477 | 478 | # 정규식으로 패턴 매칭 확인 (완전 일치) 479 | if context_ref and pattern_regex.match(context_ref): 480 | unit_ref = elem.get('unitRef') 481 | value_text = elem.text 482 | decimals = elem.get('decimals', '0') 483 | 484 | if value_text and unit_ref: 485 | try: 486 | formatted_value = format_numeric_value(value_text, decimals) 487 | extracted_data[item_name] = f"{formatted_value} ({report_type})" 488 | item_found = True 489 | break 490 | except (ValueError, TypeError) as e: 491 | pass 492 | 493 | if item_found: 494 | break 495 | 496 | except ET.ParseError as e: 497 | extracted_data = {key: "XBRL 파싱 오류" for key in items_and_tags} 498 | except Exception as e: 499 | traceback.print_exc() 500 | extracted_data = {key: "데이터 추출 오류" for key in items_and_tags} 501 | 502 | return extracted_data 503 | 504 | 505 | def determine_report_code(report_name: str) -> Optional[str]: 506 | """ 507 | 보고서 이름으로부터 보고서 코드 결정 508 | 509 | Args: 510 | report_name: 보고서 이름 511 | 512 | Returns: 513 | 해당하는 보고서 코드 또는 None 514 | """ 515 | if "사업보고서" in report_name: 516 | return REPORT_CODE["사업보고서"] 517 | elif "반기보고서" in report_name: 518 | return REPORT_CODE["반기보고서"] 519 | elif "분기보고서" in report_name: 520 | if ".03)" in report_name or "(1분기)" in report_name: 521 | return REPORT_CODE["1분기보고서"] 522 | elif ".09)" in report_name or "(3분기)" in report_name: 523 | return REPORT_CODE["3분기보고서"] 524 | 525 | return None 526 | 527 | 528 | def adjust_end_date(end_date: str) -> Tuple[str, bool]: 529 | """ 530 | 공시 제출 기간을 고려하여 종료일 조정 531 | 532 | Args: 533 | end_date: 원래 종료일 (YYYYMMDD) 534 | 535 | Returns: 536 | 조정된 종료일과 조정 여부 537 | """ 538 | try: 539 | # 입력된 end_date를 datetime 객체로 변환 540 | end_date_obj = datetime.strptime(end_date, "%Y%m%d") 541 | 542 | # 95일 추가 543 | adjusted_end_date_obj = end_date_obj + timedelta(days=95) 544 | 545 | # 현재 날짜보다 미래인 경우 현재 날짜로 조정 546 | current_date = datetime.now() 547 | if adjusted_end_date_obj > current_date: 548 | adjusted_end_date_obj = current_date 549 | 550 | # 포맷 변환하여 문자열로 반환 551 | adjusted_end_date = adjusted_end_date_obj.strftime("%Y%m%d") 552 | 553 | # 조정 여부 반환 554 | return adjusted_end_date, adjusted_end_date != end_date 555 | except Exception: 556 | # 오류 발생 시 원래 값 그대로 반환 557 | return end_date, False 558 | 559 | 560 | def extract_business_section(document_text: str, section_type: str) -> str: 561 | """ 562 | 공시서류 원본파일 텍스트에서 특정 비즈니스 섹션만 추출하는 함수 563 | 564 | Args: 565 | document_text: 공시서류 원본 텍스트 566 | section_type: 추출할 섹션 유형 567 | ('사업의 개요', '주요 제품 및 서비스', '원재료 및 생산설비', 568 | '매출 및 수주상황', '위험관리 및 파생거래', '주요계약 및 연구개발활동', 569 | '기타 참고사항') 570 | 571 | Returns: 572 | 추출된 섹션 텍스트 (태그 제거 및 정리된 상태) 573 | """ 574 | import re 575 | 576 | # SECTION 태그 형식 확인 577 | section_tags = re.findall(r']*>', document_text) 578 | section_end_tags = re.findall(r']*>', document_text) 579 | 580 | # TITLE 태그가 있는지 확인 581 | title_tags = re.findall(r']*>(.*?)', document_text) 582 | 583 | # 섹션 타입별 패턴 매핑 (번호가 포함된 경우도 처리) 584 | section_patterns = { 585 | '사업의 개요': r']*>(?:\d+\.\s*)?사업의\s*개요[^<]*(.*?)(?=]*>(?:\d+\.\s*)?주요\s*제품[^<]*(.*?)(?=]*>(?:\d+\.\s*)?원재료[^<]*(.*?)(?=]*>(?:\d+\.\s*)?매출[^<]*(.*?)(?=]*>(?:\d+\.\s*)?위험관리[^<]*(.*?)(?=', 590 | '주요계약 및 연구개발활동': r']*>(?:\d+\.\s*)?주요\s*계약[^<]*(.*?)(?=]*>(?:\d+\.\s*)?기타\s*참고사항[^<]*(.*?)(?=', 592 | } 593 | 594 | # 요청된 섹션 패턴 확인 595 | if section_type not in section_patterns: 596 | return f"지원하지 않는 섹션 유형입니다. 지원되는 유형: {', '.join(section_patterns.keys())}" 597 | 598 | # 해당 섹션과 일치하는 제목 찾기 599 | section_keyword = section_type.split(' ')[0] 600 | matching_titles = [title for title in title_tags if section_keyword.lower() in title.lower()] 601 | 602 | # 정규표현식 패턴으로 섹션 추출 시도 1: 기본 패턴 603 | pattern = section_patterns[section_type] 604 | matches = re.search(pattern, document_text, re.DOTALL | re.IGNORECASE) 605 | 606 | # 정규표현식 패턴으로 섹션 추출 시도 2: SECTION 태그 종료 패턴 수정 607 | if not matches: 608 | # SECTION-숫자 형태의 종료 태그 지원 609 | pattern = section_patterns[section_type].replace(']*>{escaped_title}(.*?)(?=]*>', ' ', section_text) # HTML 태그 제거 629 | clean_text = re.sub(r'USERMARK\s*=\s*"[^"]*"', '', clean_text) # USERMARK 제거 630 | clean_text = re.sub(r'\s+', ' ', clean_text) # 연속된 공백 제거 631 | clean_text = re.sub(r'\n\s*\n', '\n\n', clean_text) # 빈 줄 처리 632 | clean_text = clean_text.strip() # 앞뒤 공백 제거 633 | 634 | return clean_text 635 | 636 | 637 | async def extract_business_section_from_dart(rcept_no: str, section_type: str) -> str: 638 | """ 639 | DART API를 통해 공시서류를 다운로드하고 특정 비즈니스 섹션만 추출하는 함수 640 | 641 | Args: 642 | rcept_no: 공시 접수번호(14자리) 643 | section_type: 추출할 섹션 유형 644 | ('사업의 개요', '주요 제품 및 서비스', '원재료 및 생산설비', 645 | '매출 및 수주상황', '위험관리 및 파생거래', '주요계약 및 연구개발활동', 646 | '기타 참고사항') 647 | 648 | Returns: 649 | 추출된 섹션 텍스트 또는 오류 메시지 650 | """ 651 | # 원본 문서 다운로드 652 | document_text, binary_data = await get_original_document(rcept_no) 653 | 654 | # 다운로드 실패 시 655 | if binary_data is None: 656 | return f"공시서류 다운로드 실패: {document_text}" 657 | 658 | # 섹션 추출 659 | section_text = extract_business_section(document_text, section_type) 660 | 661 | return section_text 662 | 663 | 664 | async def get_original_document(rcept_no: str) -> Tuple[str, Optional[bytes]]: 665 | """ 666 | DART 공시서류 원본파일을 다운로드하여 텍스트로 변환해 반환하는 함수 667 | 668 | Args: 669 | rcept_no: 공시 접수번호(14자리) 670 | 671 | Returns: 672 | (파일 내용 문자열 또는 오류 메시지, 원본 바이너리 데이터(성공 시) 또는 None(실패 시)) 673 | """ 674 | url = f"{BASE_URL}/document.xml?crtfc_key={API_KEY}&rcept_no={rcept_no}" 675 | 676 | try: 677 | async with httpx.AsyncClient(timeout=30.0) as client: 678 | response = await client.get(url) 679 | 680 | if response.status_code != 200: 681 | return f"API 요청 실패: HTTP 상태 코드 {response.status_code}", None 682 | 683 | # API 오류 메시지 확인 시도 (XML 형식일 수 있음) 684 | try: 685 | root = ET.fromstring(response.content) 686 | status = root.findtext('status') 687 | message = root.findtext('message') 688 | if status and message: 689 | return f"DART API 오류: {status} - {message}", None 690 | except ET.ParseError: 691 | # 파싱 오류는 정상적인 ZIP 파일일 수 있으므로 계속 진행 692 | pass 693 | 694 | try: 695 | # ZIP 파일 처리 696 | with zipfile.ZipFile(BytesIO(response.content)) as zip_file: 697 | # 압축 파일 내의 파일 목록 698 | file_list = zip_file.namelist() 699 | 700 | if not file_list: 701 | return "ZIP 파일 내에 파일이 없습니다.", None 702 | 703 | # 파일명이 가장 짧은 파일 선택 (일반적으로 메인 파일일 가능성이 높음) 704 | target_file = min(file_list, key=len) 705 | file_ext = target_file.split('.')[-1].lower() 706 | 707 | # 파일 내용 읽기 708 | with zip_file.open(target_file) as doc_file: 709 | file_content = doc_file.read() 710 | 711 | # 텍스트 파일인 경우 (txt, html, xml 등) 712 | if file_ext in ['txt', 'html', 'htm', 'xml', 'xbrl']: 713 | # 다양한 인코딩 시도 714 | encodings = ['utf-8', 'euc-kr', 'cp949'] 715 | text_content = None 716 | 717 | for encoding in encodings: 718 | try: 719 | text_content = file_content.decode(encoding) 720 | break 721 | except UnicodeDecodeError: 722 | continue 723 | 724 | if text_content: 725 | return text_content, file_content 726 | else: 727 | return "파일을 텍스트로 변환할 수 없습니다 (인코딩 문제).", file_content 728 | # PDF 또는 기타 바이너리 파일 729 | else: 730 | return f"파일이 텍스트 형식이 아닙니다 (형식: {file_ext}).", file_content 731 | 732 | except zipfile.BadZipFile: 733 | return "다운로드한 파일이 유효한 ZIP 파일이 아닙니다.", None 734 | 735 | except httpx.RequestError as e: 736 | return f"API 요청 중 네트워크 오류 발생: {str(e)}", None 737 | except Exception as e: 738 | return f"공시 원본 다운로드 중 예상치 못한 오류 발생: {str(e)}", None 739 | 740 | 741 | # MCP 도구 742 | @mcp.tool() 743 | async def search_disclosure( 744 | company_name: str, 745 | start_date: str, 746 | end_date: str, 747 | ctx: Context, 748 | requested_items: Optional[List[str]] = None, 749 | ) -> str: 750 | """ 751 | 회사의 주요 재무 정보를 검색하여 제공하는 도구. 752 | requested_items가 주어지면 해당 항목 관련 데이터가 있는 공시만 필터링합니다. 753 | 754 | Args: 755 | company_name: 회사명 (예: 삼성전자, 네이버 등) 756 | start_date: 시작일 (YYYYMMDD 형식, 예: 20230101) 757 | end_date: 종료일 (YYYYMMDD 형식, 예: 20231231) 758 | ctx: MCP Context 객체 759 | requested_items: 사용자가 요청한 재무 항목 이름 리스트 (예: ["매출액", "영업이익"]). None이면 모든 주요 항목을 대상으로 함. 사용 가능한 항목: 매출액, 영업이익, 당기순이익, 영업활동 현금흐름, 투자활동 현금흐름, 재무활동 현금흐름, 자산총계, 부채총계, 자본총계 760 | 761 | Returns: 762 | 검색된 각 공시의 주요 재무 정보 요약 텍스트 (요청 항목 관련 데이터가 있는 경우만) 763 | """ 764 | # 결과 문자열 초기화 765 | result = "" 766 | 767 | try: 768 | # 진행 상황 알림 769 | info_msg = f"{company_name}의" 770 | if requested_items: 771 | info_msg += f" {', '.join(requested_items)} 관련" 772 | info_msg += " 재무 정보를 검색합니다." 773 | ctx.info(info_msg) 774 | 775 | # end_date 조정 776 | original_end_date = end_date 777 | adjusted_end_date, was_adjusted = adjust_end_date(end_date) 778 | 779 | if was_adjusted: 780 | ctx.info(f"공시 제출 기간을 고려하여 검색 종료일을 {original_end_date}에서 {adjusted_end_date}로 자동 조정했습니다.") 781 | end_date = adjusted_end_date 782 | 783 | # 회사 코드 조회 784 | corp_code, matched_name = await get_corp_code_by_name(company_name) 785 | if not corp_code: 786 | return f"회사 검색 오류: {matched_name}" 787 | 788 | ctx.info(f"{matched_name}(고유번호: {corp_code})의 공시를 검색합니다.") 789 | 790 | # 공시 목록 조회 791 | disclosures, error_msg = await get_disclosure_list(corp_code, start_date, end_date) 792 | if error_msg: 793 | return f"공시 목록 조회 오류: {error_msg}" 794 | 795 | if not disclosures: 796 | date_range_msg = f"{start_date}부터 {end_date}까지" 797 | if was_adjusted: 798 | date_range_msg += f" (원래 요청: {start_date}~{original_end_date}, 공시 제출 기간 고려하여 확장)" 799 | return f"{date_range_msg} '{matched_name}'(고유번호: {corp_code})의 정기공시가 없습니다." 800 | 801 | ctx.info(f"{len(disclosures)}개의 정기공시를 찾았습니다. XBRL 데이터 조회 및 분석을 시도합니다.") 802 | 803 | # 추출할 재무 항목 및 가능한 태그 리스트 정의 804 | all_items_and_tags = { 805 | "매출액": ["ifrs-full:Revenue"], 806 | "영업이익": ["dart:OperatingIncomeLoss"], 807 | "당기순이익": ["ifrs-full:ProfitLoss"], 808 | "영업활동 현금흐름": ["ifrs-full:CashFlowsFromUsedInOperatingActivities"], 809 | "투자활동 현금흐름": ["ifrs-full:CashFlowsFromUsedInInvestingActivities"], 810 | "재무활동 현금흐름": ["ifrs-full:CashFlowsFromUsedInFinancingActivities"], 811 | "자산총계": ["ifrs-full:Assets"], 812 | "부채총계": ["ifrs-full:Liabilities"], 813 | "자본총계": ["ifrs-full:Equity"] 814 | } 815 | 816 | # 사용자가 요청한 항목만 추출하도록 구성 817 | if requested_items: 818 | items_to_extract = {item: tags for item, tags in all_items_and_tags.items() if item in requested_items} 819 | if not items_to_extract: 820 | unsupported_items = [item for item in requested_items if item not in all_items_and_tags] 821 | return f"요청하신 항목 중 지원되지 않는 항목이 있습니다: {', '.join(unsupported_items)}. 지원 항목: {', '.join(all_items_and_tags.keys())}" 822 | else: 823 | items_to_extract = all_items_and_tags 824 | 825 | # 결과 문자열 초기화 826 | result = f"# {matched_name} 주요 재무 정보 ({start_date} ~ {end_date})\n" 827 | if requested_items: 828 | result += f"({', '.join(requested_items)} 관련)\n" 829 | result += "\n" 830 | 831 | # 최대 5개의 공시만 처리 (API 호출 제한 및 시간 고려) 832 | disclosure_count = min(5, len(disclosures)) 833 | processed_count = 0 834 | relevant_reports_found = 0 835 | api_errors = [] 836 | 837 | # 각 공시별 처리 838 | for disclosure in disclosures[:disclosure_count]: 839 | report_name = disclosure.get('report_nm', '제목 없음') 840 | rcept_dt = disclosure.get('rcept_dt', '날짜 없음') 841 | rcept_no = disclosure.get('rcept_no', '') 842 | 843 | # 보고서 코드 결정 844 | reprt_code = determine_report_code(report_name) 845 | if not rcept_no or not reprt_code: 846 | continue 847 | 848 | # 진행 상황 보고 849 | processed_count += 1 850 | await ctx.report_progress(processed_count, disclosure_count) 851 | 852 | ctx.info(f"공시 {processed_count}/{disclosure_count} 분석 중: {report_name} (접수번호: {rcept_no})") 853 | 854 | # XBRL 데이터 조회 855 | try: 856 | xbrl_text = await get_financial_statement_xbrl(rcept_no, reprt_code) 857 | 858 | # XBRL 파싱 및 데이터 추출 859 | financial_data = {} 860 | parse_error = None 861 | 862 | if not xbrl_text.startswith(("DART API 오류:", "API 요청 실패:", "ZIP 파일", "<인코딩 오류:")): 863 | try: 864 | financial_data = parse_xbrl_financial_data(xbrl_text, items_to_extract) 865 | except Exception as e: 866 | parse_error = e 867 | ctx.warning(f"XBRL 파싱/분석 중 오류 발생 ({report_name}): {e}") 868 | financial_data = {key: "분석 중 예외 발생" for key in items_to_extract} 869 | elif xbrl_text.startswith("DART API 오류: 013"): 870 | financial_data = {key: "데이터 없음(API 013)" for key in items_to_extract} 871 | else: 872 | error_summary = xbrl_text.split('\n')[0][:100] 873 | financial_data = {key: f"오류({error_summary})" for key in items_to_extract} 874 | api_errors.append(f"{report_name}: {error_summary}") 875 | 876 | # 요청된 항목 관련 데이터가 있는지 확인 877 | is_relevant = True 878 | if requested_items: 879 | is_relevant = any( 880 | item in financial_data and 881 | financial_data[item] not in INVALID_VALUE_INDICATORS and 882 | not financial_data[item].startswith("오류(") and 883 | not financial_data[item].startswith("분석 중") 884 | for item in requested_items 885 | ) 886 | 887 | # 관련 데이터가 있는 공시만 결과에 추가 888 | if is_relevant: 889 | relevant_reports_found += 1 890 | result += f"## {report_name} ({rcept_dt})\n" 891 | result += f"접수번호: {rcept_no}\n\n" 892 | 893 | if financial_data: 894 | for item, value in financial_data.items(): 895 | result += f"- {item}: {value}\n" 896 | elif parse_error: 897 | result += f"- XBRL 분석 중 오류 발생: {parse_error}\n" 898 | else: 899 | result += "- 주요 재무 정보를 추출하지 못했습니다.\n" 900 | 901 | result += "\n" + "-" * 50 + "\n\n" 902 | else: 903 | ctx.info(f"[{report_name}] 건너뜀: 요청하신 항목({', '.join(requested_items) if requested_items else '전체'}) 관련 유효 데이터 없음.") 904 | except Exception as e: 905 | ctx.error(f"공시 처리 중 예상치 못한 오류 발생 ({report_name}): {e}") 906 | api_errors.append(f"{report_name}: {str(e)}") 907 | traceback.print_exc() 908 | 909 | # 최종 결과 메시지 추가 910 | if api_errors: 911 | result += "\n## 처리 중 발생한 오류\n" 912 | for error in api_errors: 913 | result += f"- {error}\n" 914 | result += "\n" 915 | 916 | if relevant_reports_found == 0 and processed_count > 0: 917 | no_data_reason = "요청하신 항목 관련 유효한 데이터를 찾지 못했거나" if requested_items else "주요 재무 데이터를 찾지 못했거나" 918 | result += f"※ 처리된 공시에서 {no_data_reason}, 데이터가 제공되지 않는 보고서일 수 있습니다.\n" 919 | elif processed_count == 0 and disclosures: 920 | result += "조회된 정기공시가 있으나, XBRL 데이터를 포함하는 보고서 유형(사업/반기/분기)이 아니거나 처리 중 오류가 발생했습니다.\n" 921 | 922 | if len(disclosures) > disclosure_count: 923 | result += f"※ 총 {len(disclosures)}개의 정기공시 중 최신 {disclosure_count}개에 대해 분석을 시도했습니다.\n" 924 | 925 | if relevant_reports_found > 0 and requested_items: 926 | result += f"\n※ 요청하신 항목({', '.join(requested_items)}) 관련 정보가 있는 {relevant_reports_found}개의 보고서를 표시했습니다.\n" 927 | 928 | except Exception as e: 929 | return f"재무 정보 검색 중 예상치 못한 오류가 발생했습니다: {str(e)}\n\n{traceback.format_exc()}" 930 | 931 | result += chat_guideline 932 | return result.strip() 933 | 934 | 935 | @mcp.tool() 936 | async def search_detailed_financial_data( 937 | company_name: str, 938 | start_date: str, 939 | end_date: str, 940 | ctx: Context, 941 | statement_type: Optional[str] = None, 942 | ) -> str: 943 | """ 944 | 회사의 세부적인 재무 정보를 제공하는 도구. 945 | 946 | Args: 947 | company_name: 회사명 (예: 삼성전자, 네이버 등) 948 | start_date: 시작일 (YYYYMMDD 형식, 예: 20230101) 949 | end_date: 종료일 (YYYYMMDD 형식, 예: 20231231) 950 | ctx: MCP Context 객체 951 | statement_type: 재무제표 유형 ("재무상태표", "손익계산서", "현금흐름표" 중 하나 또는 None) 952 | None인 경우 모든 유형의 재무제표 정보를 반환합니다. 953 | 954 | Returns: 955 | 선택한 재무제표 유형(들)의 세부 항목 정보가 포함된 텍스트 956 | """ 957 | # 결과 문자열 초기화 958 | result = "" 959 | api_errors = [] 960 | 961 | try: 962 | # 재무제표 유형 검증 963 | if statement_type is not None and statement_type not in STATEMENT_TYPES: 964 | return f"지원하지 않는 재무제표 유형입니다. 지원되는 유형: {', '.join(STATEMENT_TYPES.keys())}" 965 | 966 | # 모든 재무제표 유형을 처리할 경우 967 | if statement_type is None: 968 | all_statement_types = list(STATEMENT_TYPES.keys()) 969 | ctx.info(f"{company_name}의 모든 재무제표(재무상태표, 손익계산서, 현금흐름표) 세부 정보를 검색합니다.") 970 | else: 971 | all_statement_types = [statement_type] 972 | ctx.info(f"{company_name}의 {statement_type} 세부 정보를 검색합니다.") 973 | 974 | # end_date 조정 975 | original_end_date = end_date 976 | adjusted_end_date, was_adjusted = adjust_end_date(end_date) 977 | 978 | if was_adjusted: 979 | ctx.info(f"공시 제출 기간을 고려하여 검색 종료일을 {original_end_date}에서 {adjusted_end_date}로 자동 조정했습니다.") 980 | end_date = adjusted_end_date 981 | 982 | # 회사 코드 조회 983 | corp_code, matched_name = await get_corp_code_by_name(company_name) 984 | if not corp_code: 985 | return f"회사 검색 오류: {matched_name}" 986 | 987 | ctx.info(f"{matched_name}(고유번호: {corp_code})의 공시를 검색합니다.") 988 | 989 | # 공시 목록 조회 990 | disclosures, error_msg = await get_disclosure_list(corp_code, start_date, end_date) 991 | if error_msg: 992 | return error_msg 993 | 994 | if not disclosures: 995 | date_range_msg = f"{start_date}부터 {end_date}까지" 996 | if was_adjusted: 997 | date_range_msg += f" (원래 요청: {start_date}~{original_end_date}, 공시 제출 기간 고려하여 확장)" 998 | return f"{date_range_msg} '{matched_name}'(고유번호: {corp_code})의 정기공시가 없습니다." 999 | 1000 | ctx.info(f"{len(disclosures)}개의 정기공시를 찾았습니다. XBRL 데이터 조회 및 분석을 시도합니다.") 1001 | 1002 | # 결과 문자열 초기화 1003 | result = f"# {matched_name}의 세부 재무 정보 ({start_date} ~ {end_date})\n\n" 1004 | 1005 | # 최대 5개의 공시만 처리 (API 호출 제한 및 시간 고려) 1006 | disclosure_count = min(5, len(disclosures)) 1007 | 1008 | # 각 공시별로 XBRL 데이터 조회 및 저장 1009 | processed_disclosures = [] 1010 | 1011 | for disclosure in disclosures[:disclosure_count]: 1012 | try: 1013 | report_name = disclosure.get('report_nm', '제목 없음') 1014 | rcept_dt = disclosure.get('rcept_dt', '날짜 없음') 1015 | rcept_no = disclosure.get('rcept_no', '') 1016 | 1017 | # 보고서 코드 결정 1018 | reprt_code = determine_report_code(report_name) 1019 | if not rcept_no or not reprt_code: 1020 | continue 1021 | 1022 | ctx.info(f"공시 분석 중: {report_name} (접수번호: {rcept_no})") 1023 | 1024 | # XBRL 데이터 조회 1025 | xbrl_text = await get_financial_statement_xbrl(rcept_no, reprt_code) 1026 | 1027 | if not xbrl_text.startswith(("DART API 오류:", "API 요청 실패:", "ZIP 파일", "<인코딩 오류:")): 1028 | processed_disclosures.append({ 1029 | 'report_name': report_name, 1030 | 'rcept_dt': rcept_dt, 1031 | 'rcept_no': rcept_no, 1032 | 'reprt_code': reprt_code, 1033 | 'xbrl_text': xbrl_text 1034 | }) 1035 | else: 1036 | error_summary = xbrl_text.split('\n')[0][:100] 1037 | api_errors.append(f"{report_name}: {error_summary}") 1038 | ctx.warning(f"XBRL 데이터 조회 오류 ({report_name}): {error_summary}") 1039 | except Exception as e: 1040 | api_errors.append(f"{report_name if 'report_name' in locals() else '알 수 없는 보고서'}: {str(e)}") 1041 | ctx.error(f"공시 데이터 처리 중 예상치 못한 오류 발생: {e}") 1042 | traceback.print_exc() 1043 | 1044 | # 각 재무제표 유형별 처리 1045 | for current_statement_type in all_statement_types: 1046 | result += f"## {current_statement_type}\n\n" 1047 | 1048 | # 해당 재무제표 유형에 대한 태그 목록 조회 1049 | items_to_extract = DETAILED_TAGS[current_statement_type] 1050 | 1051 | # 재무제표 유형별 결과 저장 1052 | reports_with_data = 0 1053 | 1054 | # 각 공시별 처리 1055 | for disclosure in processed_disclosures: 1056 | try: 1057 | report_name = disclosure['report_name'] 1058 | rcept_dt = disclosure['rcept_dt'] 1059 | rcept_no = disclosure['rcept_no'] 1060 | xbrl_text = disclosure['xbrl_text'] 1061 | 1062 | # XBRL 파싱 및 데이터 추출 1063 | try: 1064 | financial_data = parse_xbrl_financial_data(xbrl_text, items_to_extract) 1065 | 1066 | # 유효한 데이터가 있는지 확인 (최소 1개 항목 이상) 1067 | valid_items_count = sum(1 for value in financial_data.values() 1068 | if value not in INVALID_VALUE_INDICATORS 1069 | and not value.startswith("오류(") 1070 | and not value.startswith("분석 중")) 1071 | 1072 | if valid_items_count >= 1: 1073 | reports_with_data += 1 1074 | 1075 | # 데이터 결과에 추가 1076 | result += f"### {report_name} ({rcept_dt})\n" 1077 | result += f"접수번호: {rcept_no}\n\n" 1078 | 1079 | # 테이블 형식으로 데이터 출력 1080 | result += "| 항목 | 값 |\n" 1081 | result += "|------|------|\n" 1082 | 1083 | for item, value in financial_data.items(): 1084 | if value not in INVALID_VALUE_INDICATORS and not value.startswith("오류(") and not value.startswith("분석 중"): 1085 | result += f"| {item} | {value} |\n" 1086 | else: 1087 | # 매칭되지 않은 항목은 '-'로 표시 1088 | result += f"| {item} | - |\n" 1089 | 1090 | result += "\n" 1091 | else: 1092 | ctx.info(f"[{report_name}] {current_statement_type}의 유효한 데이터가 없습니다.") 1093 | except Exception as e: 1094 | ctx.warning(f"XBRL 파싱/분석 중 오류 발생 ({report_name}): {e}") 1095 | api_errors.append(f"{report_name} 분석 중 오류: {str(e)}") 1096 | except Exception as e: 1097 | ctx.error(f"공시 데이터 처리 중 예상치 못한 오류 발생: {e}") 1098 | api_errors.append(f"공시 데이터 처리 오류: {str(e)}") 1099 | traceback.print_exc() 1100 | 1101 | # 재무제표 유형별 결과 요약 1102 | if reports_with_data == 0: 1103 | result += f"조회된 공시에서 유효한 {current_statement_type} 데이터를 찾지 못했습니다.\n\n" 1104 | 1105 | result += "-" * 50 + "\n\n" 1106 | 1107 | # 최종 결과 메시지 추가 1108 | if api_errors: 1109 | result += "\n## 처리 중 발생한 오류\n" 1110 | for error in api_errors: 1111 | result += f"- {error}\n" 1112 | result += "\n" 1113 | 1114 | if len(disclosures) > disclosure_count: 1115 | result += f"※ 총 {len(disclosures)}개의 정기공시 중 최신 {disclosure_count}개에 대해 분석을 시도했습니다.\n" 1116 | 1117 | if len(processed_disclosures) == 0: 1118 | result += "※ 모든 공시에서 XBRL 데이터를 추출하는데 실패했습니다. 오류 메시지를 확인해주세요.\n" 1119 | 1120 | except Exception as e: 1121 | return f"세부 재무 정보 검색 중 예상치 못한 오류가 발생했습니다: {str(e)}\n\n{traceback.format_exc()}" 1122 | 1123 | result += chat_guideline 1124 | return result.strip() 1125 | 1126 | 1127 | @mcp.tool() 1128 | async def search_business_information( 1129 | company_name: str, 1130 | start_date: str, 1131 | end_date: str, 1132 | information_type: str, 1133 | ctx: Context, 1134 | ) -> str: 1135 | """ 1136 | 회사의 사업 관련 현황 정보를 제공하는 도구 1137 | 1138 | Args: 1139 | company_name: 회사명 (예: 삼성전자, 네이버 등) 1140 | start_date: 시작일 (YYYYMMDD 형식, 예: 20230101) 1141 | end_date: 종료일 (YYYYMMDD 형식, 예: 20231231) 1142 | information_type: 조회할 정보 유형 1143 | '사업의 개요' - 회사의 전반적인 사업 내용 1144 | '주요 제품 및 서비스' - 회사의 주요 제품과 서비스 정보 1145 | '원재료 및 생산설비' - 원재료 조달 및 생산 설비 현황 1146 | '매출 및 수주상황' - 매출과 수주 현황 정보 1147 | '위험관리 및 파생거래' - 리스크 관리 방안 및 파생상품 거래 정보 1148 | '주요계약 및 연구개발활동' - 주요 계약 현황 및 R&D 활동 1149 | '기타 참고사항' - 기타 사업 관련 참고 정보 1150 | ctx: MCP Context 객체 1151 | 1152 | Returns: 1153 | 요청한 정보 유형에 대한 해당 회사의 사업 정보 텍스트 1154 | """ 1155 | # 결과 문자열 초기화 1156 | result = "" 1157 | 1158 | try: 1159 | # 지원하는 정보 유형 검증 1160 | supported_types = [ 1161 | '사업의 개요', '주요 제품 및 서비스', '원재료 및 생산설비', 1162 | '매출 및 수주상황', '위험관리 및 파생거래', '주요계약 및 연구개발활동', 1163 | '기타 참고사항' 1164 | ] 1165 | 1166 | if information_type not in supported_types: 1167 | return f"지원하지 않는 정보 유형입니다. 지원되는 유형: {', '.join(supported_types)}" 1168 | 1169 | # 진행 상황 알림 1170 | ctx.info(f"{company_name}의 {information_type} 정보를 검색합니다.") 1171 | 1172 | # end_date 조정 1173 | original_end_date = end_date 1174 | adjusted_end_date, was_adjusted = adjust_end_date(end_date) 1175 | 1176 | if was_adjusted: 1177 | ctx.info(f"공시 제출 기간을 고려하여 검색 종료일을 {original_end_date}에서 {adjusted_end_date}로 자동 조정했습니다.") 1178 | end_date = adjusted_end_date 1179 | 1180 | # 회사 코드 조회 1181 | corp_code, matched_name = await get_corp_code_by_name(company_name) 1182 | if not corp_code: 1183 | return f"회사 검색 오류: {matched_name}" 1184 | 1185 | ctx.info(f"{matched_name}(고유번호: {corp_code})의 공시를 검색합니다.") 1186 | 1187 | # 공시 목록 조회 1188 | disclosures, error_msg = await get_disclosure_list(corp_code, start_date, end_date) 1189 | if error_msg: 1190 | return error_msg 1191 | 1192 | ctx.info(f"{len(disclosures)}개의 정기공시를 찾았습니다. 적절한 공시를 선택하여 정보를 추출합니다.") 1193 | 1194 | # 사업정보를 포함할 가능성이 높은 정기보고서를 우선순위에 따라 필터링 1195 | priority_reports = [ 1196 | "사업보고서", "반기보고서", "분기보고서" 1197 | ] 1198 | 1199 | selected_disclosure = None 1200 | 1201 | # 우선순위에 따라 공시 선택 1202 | for priority in priority_reports: 1203 | for disclosure in disclosures: 1204 | report_name = disclosure.get('report_nm', '') 1205 | if priority in report_name: 1206 | selected_disclosure = disclosure 1207 | break 1208 | if selected_disclosure: 1209 | break 1210 | 1211 | # 우선순위에 따른 공시를 찾지 못한 경우 첫 번째 공시 선택 1212 | if not selected_disclosure and disclosures: 1213 | selected_disclosure = disclosures[0] 1214 | 1215 | if not selected_disclosure: 1216 | return f"'{matched_name}'의 적절한 공시를 찾을 수 없습니다." 1217 | 1218 | # 선택된 공시 정보 1219 | report_name = selected_disclosure.get('report_nm', '제목 없음') 1220 | rcept_dt = selected_disclosure.get('rcept_dt', '날짜 없음') 1221 | rcept_no = selected_disclosure.get('rcept_no', '') 1222 | 1223 | ctx.info(f"'{report_name}' (접수번호: {rcept_no}, 접수일: {rcept_dt}) 공시에서 '{information_type}' 정보를 추출합니다.") 1224 | 1225 | # 섹션 추출 1226 | try: 1227 | section_text = await extract_business_section_from_dart(rcept_no, information_type) 1228 | 1229 | # 추출 결과 확인 1230 | if section_text.startswith(f"공시서류 다운로드 실패") or section_text.startswith(f"'{information_type}' 섹션을 찾을 수 없습니다"): 1231 | api_error = section_text 1232 | result = f"# {matched_name} - {information_type}\n\n" 1233 | result += f"## 출처: {report_name} (접수일: {rcept_dt})\n\n" 1234 | result += f"정보 추출 실패: {api_error}\n\n" 1235 | result += "다음과 같은 이유로 정보를 추출하지 못했습니다:\n" 1236 | result += "1. 해당 공시에 요청하신 정보가 포함되어 있지 않을 수 있습니다.\n" 1237 | result += "2. DART API 호출 중 오류가 발생했을 수 있습니다.\n" 1238 | result += "3. 섹션 추출 과정에서 패턴 매칭에 실패했을 수 있습니다.\n" 1239 | return result 1240 | else: 1241 | # 결과 포맷팅 1242 | result = f"# {matched_name} - {information_type}\n\n" 1243 | result += f"## 출처: {report_name} (접수일: {rcept_dt})\n\n" 1244 | result += section_text 1245 | # 텍스트가 너무 길 경우 앞부분만 반환 1246 | max_length = 5000 # 적절한 최대 길이 설정 1247 | if len(result) > max_length: 1248 | result = result[:max_length] + f"\n\n... (이하 생략, 총 {len(result)} 자)" 1249 | except Exception as e: 1250 | ctx.error(f"섹션 추출 중 예상치 못한 오류 발생: {e}") 1251 | result = f"# {matched_name} - {information_type}\n\n" 1252 | result += f"## 출처: {report_name} (접수일: {rcept_dt})\n\n" 1253 | result += f"정보 추출 중 오류 발생: {str(e)}\n\n" 1254 | result += "다음과 같은 이유로 정보를 추출하지 못했습니다:\n" 1255 | result += "1. 섹션 추출 과정에서 예외가 발생했습니다.\n" 1256 | result += "2. 오류 상세 정보: " + traceback.format_exc().replace('\n', '\n ') + "\n" 1257 | 1258 | except Exception as e: 1259 | return f"사업 정보 검색 중 예상치 못한 오류가 발생했습니다: {str(e)}\n\n{traceback.format_exc()}" 1260 | 1261 | return result 1262 | 1263 | 1264 | @mcp.tool() 1265 | async def get_current_date( 1266 | ctx: Context = None 1267 | ) -> str: 1268 | """ 1269 | 현재 날짜를 YYYYMMDD 형식으로 반환하는 도구 1270 | 1271 | Args: 1272 | ctx: MCP Context 객체 (선택 사항) 1273 | 1274 | Returns: 1275 | YYYYMMDD 형식의 현재 날짜 문자열 1276 | """ 1277 | # 현재 날짜를 YYYYMMDD 형식으로 포맷팅 1278 | formatted_date = datetime.now().strftime("%Y%m%d") 1279 | 1280 | # 컨텍스트가 제공된 경우 로그 출력 1281 | if ctx: 1282 | ctx.info(f"현재 날짜: {formatted_date}") 1283 | 1284 | return formatted_date 1285 | 1286 | 1287 | # 서버 실행 코드 1288 | if __name__ == "__main__": 1289 | mcp.run(transport='stdio') 1290 | --------------------------------------------------------------------------------