@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,47 @@
1
+ /**
2
+ * safe-zip — yauzl 래퍼. ZIP bomb / path traversal / 엔트리 수 폭발 방어.
3
+ *
4
+ * DART 가 정상 응답만 보내리라는 가정은 MITM·프록시 감염 가정 시 깨진다.
5
+ * 상한을 코드에서 강제해 프로세스 크래시·임의 파일 쓰기를 막는다.
6
+ *
7
+ * 기본 한도(공시 첨부·XBRL 기준 넉넉):
8
+ * - maxTotalBytes: 200MB (해제 후 누적)
9
+ * - maxEntryBytes: 100MB (단일 파일)
10
+ * - maxEntries: 5000
11
+ *
12
+ * 예외: corp_code 전량 덤프는 ~30MB xml 하나 → 기본 한도 안이지만 혹시 증가해도
13
+ * `options.maxTotalBytes` 로 호출자가 상향 가능.
14
+ */
15
+ export interface SafeUnzipOptions {
16
+ /** 해제 후 총 누적 크기 상한 (기본 200MB) */
17
+ maxTotalBytes?: number;
18
+ /** 단일 엔트리 크기 상한 (기본 100MB) */
19
+ maxEntryBytes?: number;
20
+ /** 엔트리 개수 상한 (기본 5000) */
21
+ maxEntries?: number;
22
+ /** true 면 해당 엔트리만 읽음. false/undefined 면 읽지 않고 skip. */
23
+ filter?: (fileName: string) => boolean;
24
+ }
25
+ export interface ZipEntryResult {
26
+ name: string;
27
+ data: Buffer;
28
+ }
29
+ /** ZIP 을 메모리로 해제. 상한 초과 시 throw. */
30
+ export declare function safeUnzipToMemory(buf: Buffer, options?: SafeUnzipOptions): Promise<ZipEntryResult[]>;
31
+ export interface SafeUnzipToDiskOptions extends SafeUnzipOptions {
32
+ /** 디렉터리 생성 안 함 (기본: 자동 생성) */
33
+ skipMkdir?: boolean;
34
+ }
35
+ export interface DiskEntryResult {
36
+ name: string;
37
+ size: number;
38
+ path: string;
39
+ }
40
+ /**
41
+ * ZIP 을 디스크로 해제. path traversal (zip slip) 방어 포함.
42
+ * entry.fileName 이 절대경로·`..` 포함·`\0` 포함이면 거부.
43
+ * 해제된 경로가 outDir 밖으로 나가면 거부.
44
+ */
45
+ export declare function safeUnzipToDisk(buf: Buffer, outDir: string, options?: SafeUnzipToDiskOptions): Promise<DiskEntryResult[]>;
46
+ /** outDir 기본 규약용 공용 함수 (호출부 편의). */
47
+ export declare function defaultXbrlOutDir(homeDir: string, rceptNo: string, reprtCode: string): string;
@@ -0,0 +1,215 @@
1
+ /**
2
+ * safe-zip — yauzl 래퍼. ZIP bomb / path traversal / 엔트리 수 폭발 방어.
3
+ *
4
+ * DART 가 정상 응답만 보내리라는 가정은 MITM·프록시 감염 가정 시 깨진다.
5
+ * 상한을 코드에서 강제해 프로세스 크래시·임의 파일 쓰기를 막는다.
6
+ *
7
+ * 기본 한도(공시 첨부·XBRL 기준 넉넉):
8
+ * - maxTotalBytes: 200MB (해제 후 누적)
9
+ * - maxEntryBytes: 100MB (단일 파일)
10
+ * - maxEntries: 5000
11
+ *
12
+ * 예외: corp_code 전량 덤프는 ~30MB xml 하나 → 기본 한도 안이지만 혹시 증가해도
13
+ * `options.maxTotalBytes` 로 호출자가 상향 가능.
14
+ */
15
+ import { mkdirSync, writeFileSync } from "node:fs";
16
+ import { resolve, sep, join } from "node:path";
17
+ import yauzl from "yauzl";
18
+ const DEFAULT_MAX_TOTAL = 200 * 1024 * 1024;
19
+ const DEFAULT_MAX_ENTRY = 100 * 1024 * 1024;
20
+ const DEFAULT_MAX_ENTRIES = 5000;
21
+ /** ZIP 을 메모리로 해제. 상한 초과 시 throw. */
22
+ export function safeUnzipToMemory(buf, options = {}) {
23
+ const maxTotal = options.maxTotalBytes ?? DEFAULT_MAX_TOTAL;
24
+ const maxEntry = options.maxEntryBytes ?? DEFAULT_MAX_ENTRY;
25
+ const maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES;
26
+ const filter = options.filter;
27
+ return new Promise((resolvePromise, reject) => {
28
+ yauzl.fromBuffer(buf, { lazyEntries: true }, (err, zip) => {
29
+ if (err || !zip)
30
+ return reject(err ?? new Error("zip open failed"));
31
+ const out = [];
32
+ let totalBytes = 0;
33
+ let entryCount = 0;
34
+ let rejected = false;
35
+ const abort = (msg) => {
36
+ if (rejected)
37
+ return;
38
+ rejected = true;
39
+ reject(new Error(`safe-unzip: ${msg}`));
40
+ try {
41
+ zip.close();
42
+ }
43
+ catch { /* ignore */ }
44
+ };
45
+ zip.on("entry", (entry) => {
46
+ if (rejected)
47
+ return;
48
+ entryCount++;
49
+ if (entryCount > maxEntries)
50
+ return abort(`엔트리 수 상한 초과 (>${maxEntries})`);
51
+ // 디렉터리 엔트리 skip
52
+ if (/\/$/.test(entry.fileName)) {
53
+ zip.readEntry();
54
+ return;
55
+ }
56
+ // 단일 엔트리 크기 선제 검사 (압축 해제 전 선언된 uncompressed size)
57
+ const declared = entry.uncompressedSize ?? 0;
58
+ if (declared > maxEntry) {
59
+ return abort(`엔트리 크기 상한 초과: ${entry.fileName} (${declared} > ${maxEntry})`);
60
+ }
61
+ if (totalBytes + declared > maxTotal) {
62
+ return abort(`총 크기 상한 초과 (>${maxTotal})`);
63
+ }
64
+ // 필터 통과 안 하면 skip
65
+ if (filter && !filter(entry.fileName)) {
66
+ zip.readEntry();
67
+ return;
68
+ }
69
+ zip.openReadStream(entry, (err2, stream) => {
70
+ if (err2 || !stream)
71
+ return abort(`stream open failed: ${err2?.message ?? "unknown"}`);
72
+ const chunks = [];
73
+ let entrySize = 0;
74
+ stream.on("data", (c) => {
75
+ if (rejected)
76
+ return;
77
+ entrySize += c.length;
78
+ // 실제 해제 시에도 재검사 (declared 가 거짓말일 수 있음)
79
+ if (entrySize > maxEntry) {
80
+ return abort(`엔트리 실제 크기 상한 초과: ${entry.fileName}`);
81
+ }
82
+ if (totalBytes + entrySize > maxTotal) {
83
+ return abort(`총 크기 상한 초과 (>${maxTotal})`);
84
+ }
85
+ chunks.push(c);
86
+ });
87
+ stream.on("end", () => {
88
+ if (rejected)
89
+ return;
90
+ totalBytes += entrySize;
91
+ out.push({ name: entry.fileName, data: Buffer.concat(chunks) });
92
+ zip.readEntry();
93
+ });
94
+ stream.on("error", (e) => abort(`stream error: ${e.message}`));
95
+ });
96
+ });
97
+ zip.on("end", () => {
98
+ if (!rejected)
99
+ resolvePromise(out);
100
+ });
101
+ zip.on("error", (e) => abort(`zip error: ${e.message}`));
102
+ zip.readEntry();
103
+ });
104
+ });
105
+ }
106
+ /**
107
+ * ZIP 을 디스크로 해제. path traversal (zip slip) 방어 포함.
108
+ * entry.fileName 이 절대경로·`..` 포함·`\0` 포함이면 거부.
109
+ * 해제된 경로가 outDir 밖으로 나가면 거부.
110
+ */
111
+ export function safeUnzipToDisk(buf, outDir, options = {}) {
112
+ const maxTotal = options.maxTotalBytes ?? DEFAULT_MAX_TOTAL;
113
+ const maxEntry = options.maxEntryBytes ?? DEFAULT_MAX_ENTRY;
114
+ const maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES;
115
+ const filter = options.filter;
116
+ const outRoot = resolve(outDir);
117
+ if (!options.skipMkdir)
118
+ mkdirSync(outRoot, { recursive: true });
119
+ return new Promise((resolvePromise, reject) => {
120
+ yauzl.fromBuffer(buf, { lazyEntries: true }, (err, zip) => {
121
+ if (err || !zip)
122
+ return reject(err ?? new Error("zip open failed"));
123
+ const out = [];
124
+ let totalBytes = 0;
125
+ let entryCount = 0;
126
+ let rejected = false;
127
+ const abort = (msg) => {
128
+ if (rejected)
129
+ return;
130
+ rejected = true;
131
+ reject(new Error(`safe-unzip: ${msg}`));
132
+ try {
133
+ zip.close();
134
+ }
135
+ catch { /* ignore */ }
136
+ };
137
+ zip.on("entry", (entry) => {
138
+ if (rejected)
139
+ return;
140
+ entryCount++;
141
+ if (entryCount > maxEntries)
142
+ return abort(`엔트리 수 상한 초과 (>${maxEntries})`);
143
+ if (/\/$/.test(entry.fileName)) {
144
+ zip.readEntry();
145
+ return;
146
+ }
147
+ // zip slip 검증
148
+ const name = entry.fileName;
149
+ if (name.includes("\0") ||
150
+ name.startsWith("/") ||
151
+ name.startsWith("\\") ||
152
+ /^[a-zA-Z]:[\\/]/.test(name) || // Windows drive (C:\, C:/)
153
+ name.split(/[\\/]/).some((seg) => seg === "..")) {
154
+ return abort(`허용되지 않는 엔트리 경로: ${name}`);
155
+ }
156
+ // 해제 경로가 outRoot 하위인지 확인 (symlink·normalize 우회 방어)
157
+ const target = resolve(outRoot, name);
158
+ const rootWithSep = outRoot.endsWith(sep) ? outRoot : outRoot + sep;
159
+ if (!target.startsWith(rootWithSep) && target !== outRoot) {
160
+ return abort(`엔트리가 outDir 밖으로 탈출: ${name}`);
161
+ }
162
+ const declared = entry.uncompressedSize ?? 0;
163
+ if (declared > maxEntry)
164
+ return abort(`엔트리 크기 상한: ${name}`);
165
+ if (totalBytes + declared > maxTotal)
166
+ return abort(`총 크기 상한 초과`);
167
+ if (filter && !filter(name)) {
168
+ zip.readEntry();
169
+ return;
170
+ }
171
+ zip.openReadStream(entry, (err2, stream) => {
172
+ if (err2 || !stream)
173
+ return abort(`stream open failed: ${err2?.message ?? "unknown"}`);
174
+ const chunks = [];
175
+ let entrySize = 0;
176
+ stream.on("data", (c) => {
177
+ if (rejected)
178
+ return;
179
+ entrySize += c.length;
180
+ if (entrySize > maxEntry)
181
+ return abort(`엔트리 실제 크기 상한: ${name}`);
182
+ if (totalBytes + entrySize > maxTotal)
183
+ return abort(`총 크기 상한 초과`);
184
+ chunks.push(c);
185
+ });
186
+ stream.on("end", () => {
187
+ if (rejected)
188
+ return;
189
+ totalBytes += entrySize;
190
+ // 엔트리가 하위 디렉터리를 포함하면 생성
191
+ const dirSep = target.lastIndexOf(sep);
192
+ if (dirSep > outRoot.length) {
193
+ mkdirSync(target.substring(0, dirSep), { recursive: true });
194
+ }
195
+ const data = Buffer.concat(chunks);
196
+ writeFileSync(target, data);
197
+ out.push({ name, size: data.length, path: target });
198
+ zip.readEntry();
199
+ });
200
+ stream.on("error", (e) => abort(`stream error: ${e.message}`));
201
+ });
202
+ });
203
+ zip.on("end", () => {
204
+ if (!rejected)
205
+ resolvePromise(out);
206
+ });
207
+ zip.on("error", (e) => abort(`zip error: ${e.message}`));
208
+ zip.readEntry();
209
+ });
210
+ });
211
+ }
212
+ /** outDir 기본 규약용 공용 함수 (호출부 편의). */
213
+ export function defaultXbrlOutDir(homeDir, rceptNo, reprtCode) {
214
+ return join(homeDir, ".korean-dart-mcp", "xbrl", `${rceptNo}_${reprtCode}`);
215
+ }
@@ -0,0 +1,2 @@
1
+ export declare const VERSION = "0.9.2";
2
+ export declare const SERVER_NAME = "korean-dart-mcp";
@@ -0,0 +1,2 @@
1
+ export const VERSION = "0.9.2";
2
+ export const SERVER_NAME = "korean-dart-mcp";
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@mseep/korean-dart-mcp",
3
+ "version": "0.9.3",
4
+ "description": "OpenDART 83\uac1c API \u2192 15\uac1c MCP \ub3c4\uad6c. \uae08\uc735\uac10\ub3c5\uc6d0 \uc804\uc790\uacf5\uc2dc(DART)\ub97c AI\ub85c \uac80\uc0c9\u00b7\uc870\ud68c\u00b7\ubd84\uc11d",
5
+ "type": "module",
6
+ "main": "build/index.js",
7
+ "types": "build/index.d.ts",
8
+ "bin": {
9
+ "korean-dart-mcp": "build/index.js",
10
+ "korean-dart": "build/cli.js"
11
+ },
12
+ "scripts": {
13
+ "build": "tsc",
14
+ "watch": "tsc --watch",
15
+ "start": "node build/index.js",
16
+ "cli": "node build/cli.js",
17
+ "prepublishOnly": "npm run build"
18
+ },
19
+ "keywords": [
20
+ "mcp",
21
+ "mcp-server",
22
+ "dart",
23
+ "opendart",
24
+ "\uacf5\uc2dc",
25
+ "\uc804\uc790\uacf5\uc2dc",
26
+ "\uae08\uc735\uac10\ub3c5\uc6d0",
27
+ "claude",
28
+ "model-context-protocol",
29
+ "finance-ai",
30
+ "korean-stocks",
31
+ "xbrl",
32
+ "mseep"
33
+ ],
34
+ "author": "Chris",
35
+ "license": "MIT",
36
+ "dependencies": {
37
+ "@modelcontextprotocol/sdk": "^1.27.1",
38
+ "@xmldom/xmldom": "^0.9.8",
39
+ "better-sqlite3": "^11.7.0",
40
+ "commander": "^14.0.3",
41
+ "dotenv": "^17.3.1",
42
+ "iconv-lite": "^0.7.2",
43
+ "kordoc": "^3.0.0",
44
+ "pdfjs-dist": "^4.10.38",
45
+ "yauzl": "^3.2.0",
46
+ "zod": "^3.25.76 || ^4.0.0"
47
+ },
48
+ "devDependencies": {
49
+ "@types/better-sqlite3": "^7.6.12",
50
+ "@types/node": "^22.19.3",
51
+ "@types/yauzl": "^2.10.3",
52
+ "typescript": "^5.9.3"
53
+ },
54
+ "files": [
55
+ "build",
56
+ "README.md",
57
+ "LICENSE"
58
+ ],
59
+ "repository": {
60
+ "type": "git",
61
+ "url": "git+https://github.com/chrisryugj/korean-dart-mcp.git"
62
+ },
63
+ "engines": {
64
+ "node": ">=20.19.0"
65
+ },
66
+ "publisher": "mseep"
67
+ }