@mseep/korean-dart-mcp 0.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +660 -0
  3. package/build/cli.d.ts +2 -0
  4. package/build/cli.js +22 -0
  5. package/build/index.d.ts +2 -0
  6. package/build/index.js +36 -0
  7. package/build/lib/corp-code.d.ts +53 -0
  8. package/build/lib/corp-code.js +235 -0
  9. package/build/lib/dart-client.d.ts +25 -0
  10. package/build/lib/dart-client.js +71 -0
  11. package/build/lib/dart-xml.d.ts +26 -0
  12. package/build/lib/dart-xml.js +187 -0
  13. package/build/lib/xbrl-parser.d.ts +145 -0
  14. package/build/lib/xbrl-parser.js +673 -0
  15. package/build/server/mcp-server.d.ts +7 -0
  16. package/build/server/mcp-server.js +40 -0
  17. package/build/setup.d.ts +8 -0
  18. package/build/setup.js +264 -0
  19. package/build/tools/_helpers.d.ts +28 -0
  20. package/build/tools/_helpers.js +35 -0
  21. package/build/tools/buffett-quality-snapshot.d.ts +10 -0
  22. package/build/tools/buffett-quality-snapshot.js +261 -0
  23. package/build/tools/disclosure-anomaly.d.ts +14 -0
  24. package/build/tools/disclosure-anomaly.js +231 -0
  25. package/build/tools/download-document.d.ts +14 -0
  26. package/build/tools/download-document.js +89 -0
  27. package/build/tools/get-attachments.d.ts +15 -0
  28. package/build/tools/get-attachments.js +339 -0
  29. package/build/tools/get-company.d.ts +7 -0
  30. package/build/tools/get-company.js +32 -0
  31. package/build/tools/get-corporate-event.d.ts +14 -0
  32. package/build/tools/get-corporate-event.js +180 -0
  33. package/build/tools/get-executive-compensation.d.ts +13 -0
  34. package/build/tools/get-executive-compensation.js +89 -0
  35. package/build/tools/get-financials.d.ts +15 -0
  36. package/build/tools/get-financials.js +127 -0
  37. package/build/tools/get-major-holdings.d.ts +10 -0
  38. package/build/tools/get-major-holdings.js +117 -0
  39. package/build/tools/get-periodic-report.d.ts +7 -0
  40. package/build/tools/get-periodic-report.js +100 -0
  41. package/build/tools/get-shareholders.d.ts +13 -0
  42. package/build/tools/get-shareholders.js +87 -0
  43. package/build/tools/get-xbrl.d.ts +12 -0
  44. package/build/tools/get-xbrl.js +96 -0
  45. package/build/tools/index.d.ts +38 -0
  46. package/build/tools/index.js +66 -0
  47. package/build/tools/insider-signal.d.ts +15 -0
  48. package/build/tools/insider-signal.js +208 -0
  49. package/build/tools/resolve-corp-code.d.ts +7 -0
  50. package/build/tools/resolve-corp-code.js +40 -0
  51. package/build/tools/search-disclosures.d.ts +14 -0
  52. package/build/tools/search-disclosures.js +300 -0
  53. package/build/utils/safe-zip.d.ts +47 -0
  54. package/build/utils/safe-zip.js +215 -0
  55. package/build/version.d.ts +2 -0
  56. package/build/version.js +2 -0
  57. package/package.json +67 -0
@@ -0,0 +1,231 @@
1
+ /**
2
+ * disclosure_anomaly — 공시·회계 이상 징후 탐지 (킬러 포인트)
3
+ *
4
+ * 공시 이력과 감사인·감사의견 이력을 교차해 회계·거버넌스 위험 신호를 점수화.
5
+ *
6
+ * 시그널:
7
+ * 1. 정정공시 비율 — report_nm 에 "[기재정정]"/"[첨부정정]" 포함 비율
8
+ * 2. 감사인 교체 — 지정 연도 범위의 감사인 이름이 바뀌면 +
9
+ * 3. 감사의견 비적정 — "적정" 이외 의견(한정/부적정/의견거절)
10
+ * 4. 자본 스트레스 — 유상증자·CB·자사주 처분 공시 빈도
11
+ *
12
+ * 점수 0-100. 해석은 LLM 에게 위임하되, 핵심 flag 와 evidence 를 구조화해서 제공.
13
+ */
14
+ import { z } from "zod";
15
+ import { defineTool, normalizeDate, resolveCorp } from "./_helpers.js";
16
+ const Input = z.object({
17
+ corp: z.string().min(1).describe("회사명/종목코드/corp_code"),
18
+ start: z.string().optional().describe("기간 시작 (기본: 3년 전)"),
19
+ end: z.string().optional().describe("기간 종료 (기본: 오늘)"),
20
+ audit_years: z
21
+ .array(z.number().int().min(2015))
22
+ .optional()
23
+ .describe("감사인·의견 비교할 연도 (미지정 시 기간의 최근 3년)"),
24
+ });
25
+ function ymd(d) {
26
+ return d.toISOString().slice(0, 10).replace(/-/g, "");
27
+ }
28
+ function defaultRange(args) {
29
+ const end = args.end ? normalizeDate(args.end) : ymd(new Date());
30
+ let bgn;
31
+ if (args.start) {
32
+ bgn = normalizeDate(args.start);
33
+ }
34
+ else {
35
+ const d = new Date();
36
+ d.setFullYear(d.getFullYear() - 3);
37
+ bgn = ymd(d);
38
+ }
39
+ return { bgn_de: bgn, end_de: end };
40
+ }
41
+ export const disclosureAnomalyTool = defineTool({
42
+ name: "disclosure_anomaly",
43
+ description: "회계·거버넌스 이상 징후 스코어: 정정공시 비율, 감사인 교체, 감사의견 비적정, 자본 스트레스. " +
44
+ "점수 0-100 + 개별 flag 와 evidence 를 구조화해 반환. " +
45
+ "LLM 이 판단을 내릴 수 있는 데이터 프레임 제공 (직접 권고하지 않음).",
46
+ input: Input,
47
+ handler: async (ctx, args) => {
48
+ const record = resolveCorp(ctx.resolver, args.corp);
49
+ const { bgn_de, end_de } = defaultRange(args);
50
+ // 1. 공시 목록 수집 (페이지 순회)
51
+ const disclosures = [];
52
+ let page_no = 1;
53
+ while (true) {
54
+ const raw = await ctx.client.getJson("list.json", {
55
+ corp_code: record.corp_code,
56
+ bgn_de,
57
+ end_de,
58
+ page_no,
59
+ page_count: 100,
60
+ });
61
+ if (raw.status !== "000") {
62
+ if (raw.status === "013")
63
+ break; // 검색 결과 없음
64
+ throw new Error(`DART list 오류 [${raw.status}]: ${raw.message}`);
65
+ }
66
+ disclosures.push(...(raw.list ?? []));
67
+ if (!raw.total_page || page_no >= raw.total_page)
68
+ break;
69
+ page_no++;
70
+ if (page_no > 20)
71
+ break; // 상한
72
+ }
73
+ // 정정공시 비율
74
+ const amendments = disclosures.filter((d) => /\[(기재정정|첨부정정|첨부추가)\]/.test(d.report_nm));
75
+ const amendment_ratio = disclosures.length
76
+ ? amendments.length / disclosures.length
77
+ : 0;
78
+ // 자본 스트레스 공시 (이벤트 키워드)
79
+ const capitalStress = disclosures.filter((d) => /(유상증자|전환사채|신주인수권부사채|교환사채|자기주식 처분)/.test(d.report_nm));
80
+ // 2. 감사인·의견 시계열
81
+ const endYear = parseInt(end_de.slice(0, 4), 10);
82
+ const years = args.audit_years && args.audit_years.length > 0
83
+ ? args.audit_years.slice().sort((a, b) => a - b)
84
+ : [endYear - 2, endYear - 1, endYear];
85
+ const auditResults = await Promise.all(years.map(async (year) => {
86
+ try {
87
+ const raw = await ctx.client.getJson("accnutAdtorNmNdAdtOpinion.json", {
88
+ corp_code: record.corp_code,
89
+ bsns_year: String(year),
90
+ reprt_code: "11011",
91
+ });
92
+ if (raw.status !== "000") {
93
+ return { year, status: raw.status, message: raw.message, items: [] };
94
+ }
95
+ return { year, status: raw.status, items: raw.list ?? [] };
96
+ }
97
+ catch (e) {
98
+ return {
99
+ year,
100
+ error: e instanceof Error ? e.message : String(e),
101
+ items: [],
102
+ };
103
+ }
104
+ }));
105
+ const auditTimeline = auditResults.map((r) => ({
106
+ year: r.year,
107
+ auditor: r.items[0]?.adt_adtor ?? null,
108
+ opinion: r.items[0]?.adt_opinion ?? null,
109
+ emphasis: r.items[0]?.em_ph ?? null,
110
+ status: r.status ?? null,
111
+ message: r.message ?? null,
112
+ }));
113
+ const auditors = auditTimeline.map((a) => a.auditor).filter((x) => Boolean(x));
114
+ const uniqueAuditors = Array.from(new Set(auditors));
115
+ const auditor_changes = Math.max(0, uniqueAuditors.length - 1);
116
+ const nonCleanOpinions = auditTimeline.filter((a) => a.opinion && !/(적정|unqualified)/i.test(a.opinion));
117
+ // 3. 점수 계산 (0-100)
118
+ let score = 0;
119
+ const flags = [];
120
+ if (amendment_ratio > 0.2) {
121
+ flags.push({
122
+ flag: "high_amendment_ratio",
123
+ points: 30,
124
+ evidence: { ratio: amendment_ratio, total: disclosures.length, amended: amendments.length },
125
+ });
126
+ score += 30;
127
+ }
128
+ else if (amendment_ratio > 0.1) {
129
+ flags.push({
130
+ flag: "elevated_amendment_ratio",
131
+ points: 15,
132
+ evidence: { ratio: amendment_ratio, total: disclosures.length, amended: amendments.length },
133
+ });
134
+ score += 15;
135
+ }
136
+ if (auditor_changes >= 1) {
137
+ const pts = auditor_changes >= 2 ? 30 : 20;
138
+ flags.push({
139
+ flag: "auditor_change",
140
+ points: pts,
141
+ evidence: { changes: auditor_changes, timeline: auditTimeline },
142
+ });
143
+ score += pts;
144
+ }
145
+ if (nonCleanOpinions.length > 0) {
146
+ flags.push({
147
+ flag: "non_clean_audit_opinion",
148
+ points: 40,
149
+ evidence: nonCleanOpinions,
150
+ });
151
+ score += 40;
152
+ }
153
+ if (capitalStress.length >= 3) {
154
+ flags.push({
155
+ flag: "capital_stress_cluster",
156
+ points: 10,
157
+ evidence: {
158
+ count: capitalStress.length,
159
+ samples: capitalStress.slice(0, 5).map((d) => ({
160
+ rcept_no: d.rcept_no,
161
+ report_nm: d.report_nm,
162
+ rcept_dt: d.rcept_dt,
163
+ })),
164
+ },
165
+ });
166
+ score += 10;
167
+ }
168
+ score = Math.min(100, score);
169
+ const verdict = score >= 70 ? "red_flag" : score >= 40 ? "warning" : score >= 15 ? "watch" : "clean";
170
+ const summary_text = buildAnomalySummary({
171
+ corpName: record.corp_name,
172
+ score,
173
+ verdict,
174
+ flags: flags.map((f) => f.flag),
175
+ stats: {
176
+ disclosures_total: disclosures.length,
177
+ amendments: amendments.length,
178
+ amendment_ratio,
179
+ capital_stress_filings: capitalStress.length,
180
+ auditor_changes,
181
+ non_clean_opinions: nonCleanOpinions.length,
182
+ },
183
+ bgn_de,
184
+ end_de,
185
+ auditors: uniqueAuditors,
186
+ });
187
+ return {
188
+ resolved: record,
189
+ period: { start: bgn_de, end: end_de },
190
+ audit_years: years,
191
+ score,
192
+ verdict,
193
+ summary_text,
194
+ flags,
195
+ stats: {
196
+ disclosures_total: disclosures.length,
197
+ amendments: amendments.length,
198
+ amendment_ratio: Number(amendment_ratio.toFixed(3)),
199
+ capital_stress_filings: capitalStress.length,
200
+ auditor_changes,
201
+ unique_auditors: uniqueAuditors,
202
+ },
203
+ audit_timeline: auditTimeline,
204
+ note: "verdict 는 휴리스틱. 실제 투자 판단은 원문(download_document) 확인 필수.",
205
+ };
206
+ },
207
+ });
208
+ function buildAnomalySummary(p) {
209
+ const verdictKo = {
210
+ red_flag: "🚨 적색경보",
211
+ warning: "⚠️ 경고",
212
+ watch: "👀 관찰",
213
+ clean: "✅ 이상 없음",
214
+ };
215
+ const head = `${p.corpName} (${p.bgn_de}~${p.end_de}): ${verdictKo[p.verdict] ?? p.verdict}, 점수 ${p.score}/100.`;
216
+ const parts = [];
217
+ if (p.stats.amendments > 0) {
218
+ parts.push(`정정공시 ${p.stats.amendments}/${p.stats.disclosures_total}건 (${(p.stats.amendment_ratio * 100).toFixed(1)}%)`);
219
+ }
220
+ if (p.stats.auditor_changes > 0) {
221
+ parts.push(`감사인 ${p.stats.auditor_changes}회 교체 (${p.auditors.join(" → ")})`);
222
+ }
223
+ if (p.stats.non_clean_opinions > 0) {
224
+ parts.push(`비적정 감사의견 ${p.stats.non_clean_opinions}건`);
225
+ }
226
+ if (p.stats.capital_stress_filings >= 3) {
227
+ parts.push(`자본 스트레스 공시 ${p.stats.capital_stress_filings}건`);
228
+ }
229
+ const body = parts.length > 0 ? ` 주요 지표: ${parts.join(" · ")}.` : " 모든 지표 정상 범위.";
230
+ return head + body;
231
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * download_document — 공시 원문 XML (document.xml) + 마크다운 변환
3
+ *
4
+ * ZIP 으로 반환되는 DART 원문(DART 전용 XML 마크업) 을 해제하여 반환.
5
+ * format:
6
+ * - "markdown" (기본) → 자체 DART XML 파서로 깔끔한 마크다운 (heading/테이블 보존)
7
+ * - "raw" → 원본 XML 문자열
8
+ * - "text" → 태그 제거한 plain text (테이블 구조 사라짐)
9
+ *
10
+ * 파일이 크면 변환 후 `truncate_at` 으로 절단 (기본 100k chars).
11
+ *
12
+ * 참고: https://opendart.fss.or.kr/guide/detail.do?apiGrpCd=DS001&apiId=2019003
13
+ */
14
+ export declare const downloadDocumentTool: import("./_helpers.js").ToolDef;
@@ -0,0 +1,89 @@
1
+ /**
2
+ * download_document — 공시 원문 XML (document.xml) + 마크다운 변환
3
+ *
4
+ * ZIP 으로 반환되는 DART 원문(DART 전용 XML 마크업) 을 해제하여 반환.
5
+ * format:
6
+ * - "markdown" (기본) → 자체 DART XML 파서로 깔끔한 마크다운 (heading/테이블 보존)
7
+ * - "raw" → 원본 XML 문자열
8
+ * - "text" → 태그 제거한 plain text (테이블 구조 사라짐)
9
+ *
10
+ * 파일이 크면 변환 후 `truncate_at` 으로 절단 (기본 100k chars).
11
+ *
12
+ * 참고: https://opendart.fss.or.kr/guide/detail.do?apiGrpCd=DS001&apiId=2019003
13
+ */
14
+ import { z } from "zod";
15
+ import iconv from "iconv-lite";
16
+ import { defineTool } from "./_helpers.js";
17
+ import { dartXmlToMarkdown } from "../lib/dart-xml.js";
18
+ import { safeUnzipToMemory } from "../utils/safe-zip.js";
19
+ const Input = z.object({
20
+ rcept_no: z
21
+ .string()
22
+ .regex(/^\d{14}$/)
23
+ .describe("접수번호 14자리 (search_disclosures 의 rcept_no)"),
24
+ format: z
25
+ .enum(["markdown", "raw", "text"])
26
+ .default("markdown")
27
+ .describe("출력 포맷. markdown=DART XML → 마크다운, raw=원본 XML, text=태그 제거"),
28
+ truncate_at: z
29
+ .number()
30
+ .int()
31
+ .min(1000)
32
+ .default(100000)
33
+ .describe("텍스트 최대 길이 (초과분은 잘림)"),
34
+ });
35
+ export const downloadDocumentTool = defineTool({
36
+ name: "download_document",
37
+ description: "공시서류 원문을 마크다운/원본XML/plain text 로 반환합니다. 기본값 markdown — DART 전용 XML 을 자체 파서로 heading·테이블 보존해 변환. " +
38
+ "대형 사업보고서는 수백 KB 이상이라 기본 10만 자에서 절단. " +
39
+ "사업보고서·반기보고서·주요사항보고 등 모든 공시 원문에 사용.",
40
+ input: Input,
41
+ handler: async (_ctx, args) => {
42
+ const buf = await _ctx.client.getZip("document.xml", {
43
+ rcept_no: args.rcept_no,
44
+ });
45
+ const files = await extractZipEntries(buf);
46
+ const xmlFile = files.find((f) => /\.xml$/i.test(f.name));
47
+ if (!xmlFile) {
48
+ throw new Error(`원문 XML 을 찾지 못했습니다. 반환된 파일: ${files.map((f) => f.name).join(", ")}`);
49
+ }
50
+ // DART 원문은 EUC-KR 로 인코딩된 경우가 많음 → XML 선언에서 인코딩 감지
51
+ const xml = decodeXml(xmlFile.data);
52
+ let content;
53
+ if (args.format === "raw") {
54
+ content = xml;
55
+ }
56
+ else if (args.format === "text") {
57
+ content = xml
58
+ .replace(/<[^>]+>/g, " ")
59
+ .replace(/\s+/g, " ")
60
+ .trim();
61
+ }
62
+ else {
63
+ content = dartXmlToMarkdown(xml);
64
+ }
65
+ const truncated = content.length > args.truncate_at;
66
+ return {
67
+ rcept_no: args.rcept_no,
68
+ file: xmlFile.name,
69
+ format: args.format,
70
+ size_bytes: xmlFile.data.length,
71
+ raw_char_count: xml.length,
72
+ char_count: content.length,
73
+ truncated,
74
+ content: truncated ? content.slice(0, args.truncate_at) : content,
75
+ };
76
+ },
77
+ });
78
+ function extractZipEntries(buf) {
79
+ return safeUnzipToMemory(buf);
80
+ }
81
+ function decodeXml(buf) {
82
+ // XML 선언의 encoding 속성 확인 (첫 200바이트만)
83
+ const head = buf.subarray(0, Math.min(200, buf.length)).toString("ascii");
84
+ const m = /encoding\s*=\s*["']([^"']+)["']/i.exec(head);
85
+ const enc = (m?.[1] ?? "utf-8").toLowerCase();
86
+ if (enc === "utf-8" || enc === "utf8")
87
+ return buf.toString("utf8");
88
+ return iconv.decode(buf, enc);
89
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * get_attachments — 공시 첨부파일 목록 조회 + HWP/PDF → 마크다운 추출
3
+ *
4
+ * OpenDART API 는 첨부파일 직접 엔드포인트가 없다. DART 뷰어 HTML 을 스크래핑해야 한다.
5
+ * OpenDartReader 도 동일 패턴. 사실상 업계 표준.
6
+ *
7
+ * 1. 뷰어 HTML `/dsaf001/main.do?rcpNo=...` → JS 변수 `node1['dcmNo']` 추출
8
+ * 2. 다운로드 페이지 `/pdf/download/main.do?rcp_no=&dcm_no=` → `<td class="tL">` 파싱
9
+ * 3. 각 첨부 URL (`/pdf/download/pdf.do?...` 등) fetch → Buffer
10
+ * 4. kordoc.parse() 로 HWP/HWPX/PDF/DOCX/XLSX → 마크다운
11
+ *
12
+ * mode="list" — 첨부 목록만 반환 (가볍고 빠름, 파일명·download_url·format 힌트)
13
+ * mode="extract" — 지정한 파일을 다운로드·파싱해 마크다운 반환
14
+ */
15
+ export declare const getAttachmentsTool: import("./_helpers.js").ToolDef;