my-test-wldnjs2 0.1.0__tar.gz

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.
@@ -0,0 +1 @@
1
+ 3.14.3
@@ -0,0 +1,13 @@
1
+ Metadata-Version: 2.4
2
+ Name: my-test-wldnjs2
3
+ Version: 0.1.0
4
+ Summary: Add your description here
5
+ Requires-Python: >=3.14.3
6
+ Requires-Dist: beautifulsoup4>=4.15.0
7
+ Requires-Dist: build>=1.5.0
8
+ Requires-Dist: mcp[cli]>=1.28.0
9
+ Requires-Dist: pypdf2>=3.0.1
10
+ Requires-Dist: python-docx>=1.2.0
11
+ Requires-Dist: python-pptx>=1.0.2
12
+ Requires-Dist: requests>=2.34.2
13
+ Requires-Dist: twine>=6.2.0
File without changes
@@ -0,0 +1,23 @@
1
+ [project]
2
+ name = "my-test-wldnjs2"
3
+ version = "0.1.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ requires-python = ">=3.14.3"
7
+ dependencies = [
8
+ "beautifulsoup4>=4.15.0",
9
+ "build>=1.5.0",
10
+ "mcp[cli]>=1.28.0",
11
+ "pypdf2>=3.0.1",
12
+ "python-docx>=1.2.0",
13
+ "python-pptx>=1.0.2",
14
+ "requests>=2.34.2",
15
+ "twine>=6.2.0",
16
+ ]
17
+
18
+ [project.scripts]
19
+ my-test-wldnjs2 = "my_test_wldnjs2.server:main"
20
+
21
+ [build-system]
22
+ requires = ["hatchling"]
23
+ build-backend = "hatchling.build"
@@ -0,0 +1,2 @@
1
+ def main() -> None:
2
+ print("Hello from my-test-wldnjs2!")
@@ -0,0 +1,264 @@
1
+ import os
2
+ import sys # 추가됨: 표준 에러(stderr) 출력을 위해 필요
3
+ import sqlite3
4
+ import concurrent.futures
5
+ from datetime import datetime, timedelta
6
+ import PyPDF2
7
+ import docx
8
+ import pptx
9
+ from mcp.server.fastmcp import FastMCP
10
+
11
+ # Initialize FastMCP server
12
+ mcp = FastMCP("local_file_explorer")
13
+
14
+ # 데이터베이스 경로 설정 (사용자 홈 디렉토리에 숨김 파일로 저장)
15
+ DB_PATH = os.path.expanduser("~/.local_explorer_index.db")
16
+
17
+ def get_default_dir() -> str:
18
+ """사용자의 기본 문서 폴더 경로를 반환합니다."""
19
+ return os.path.join(os.path.expanduser("~"), "Documents")
20
+
21
+ def init_db():
22
+ """SQLite FTS5 및 메타데이터 테이블 초기화"""
23
+ with sqlite3.connect(DB_PATH) as conn:
24
+ # 파일 메타데이터 관리용 테이블
25
+ conn.execute('''
26
+ CREATE TABLE IF NOT EXISTS file_meta (
27
+ file_path TEXT PRIMARY KEY,
28
+ last_modified REAL
29
+ )
30
+ ''')
31
+ # 내용 전문 검색용 FTS5 가상 테이블
32
+ conn.execute('''
33
+ CREATE VIRTUAL TABLE IF NOT EXISTS file_content
34
+ USING fts5(file_path UNINDEXED, content)
35
+ ''')
36
+
37
+ def extract_text_safe(file_path: str, ext: str, max_pages: int = 30) -> str:
38
+ """안전하게 텍스트를 추출하며 일정량 이상 넘어가면 조기 종료합니다."""
39
+ text = ""
40
+ try:
41
+ if ext == '.pdf':
42
+ with open(file_path, 'rb') as f:
43
+ reader = PyPDF2.PdfReader(f)
44
+ for i, page in enumerate(reader.pages):
45
+ if i >= max_pages: break # 조기 종료
46
+ extracted = page.extract_text()
47
+ if extracted: text += extracted + " "
48
+ elif ext == '.docx':
49
+ doc = docx.Document(file_path)
50
+ for i, para in enumerate(doc.paragraphs):
51
+ if i >= max_pages * 15: break # 대략적인 문단 수 제한
52
+ text += para.text + " "
53
+ elif ext == '.pptx':
54
+ prs = pptx.Presentation(file_path)
55
+ for i, slide in enumerate(prs.slides):
56
+ if i >= max_pages: break
57
+ for shape in slide.shapes:
58
+ if hasattr(shape, "text"):
59
+ text += shape.text + " "
60
+ elif ext == '.txt':
61
+ with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
62
+ text = f.read(50000) # 최대 5만 자 제한
63
+ except Exception as e:
64
+ # 파일 읽기 실패 시 stderr로 로그를 남겨 MCP 통신 방해를 막음
65
+ print(f"Error reading {file_path}: {e}", file=sys.stderr)
66
+ return ""
67
+ return text
68
+
69
+
70
+ @mcp.tool()
71
+ def update_file_index(directory: str = None, extensions: list[str] = ['.pdf', '.docx', '.pptx', '.txt']) -> dict:
72
+ """
73
+ [관리 도구] 새로 추가되거나 수정된 파일의 텍스트를 추출하여 빠른 검색을 위해 인덱싱합니다.
74
+ 검색을 수행하기 전에 문서 폴더의 내용을 최신화할 때 사용하세요.
75
+ """
76
+ search_dir = directory or get_default_dir()
77
+ init_db()
78
+
79
+ files_to_process = []
80
+
81
+ # 1. 변경된 파일 스캔 (증분 인덱싱 대상 찾기)
82
+ with sqlite3.connect(DB_PATH) as conn:
83
+ cursor = conn.cursor()
84
+ for root, _, files in os.walk(search_dir):
85
+ for file in files:
86
+ ext = os.path.splitext(file)[1].lower()
87
+ if ext in extensions:
88
+ file_path = os.path.join(root, file)
89
+ try:
90
+ mtime = os.path.getmtime(file_path)
91
+ cursor.execute('SELECT last_modified FROM file_meta WHERE file_path = ?', (file_path,))
92
+ row = cursor.fetchone()
93
+ # DB에 없거나, 파일 수정 날짜가 더 최신인 경우만 처리 대상
94
+ if row is None or row[0] < mtime:
95
+ files_to_process.append((file_path, ext, mtime))
96
+ except OSError:
97
+ continue
98
+
99
+ if not files_to_process:
100
+ return {"status": "success", "message": "모든 파일이 이미 최신 상태입니다.", "indexed_count": 0}
101
+
102
+ indexed_count = 0
103
+ skipped_count = 0
104
+
105
+ # 2. ProcessPoolExecutor를 이용한 장애 격리(Isolation) 및 타임아웃
106
+ with concurrent.futures.ProcessPoolExecutor() as executor:
107
+ future_to_file = {
108
+ executor.submit(extract_text_safe, path, ext): (path, mtime)
109
+ for path, ext, mtime in files_to_process
110
+ }
111
+
112
+ with sqlite3.connect(DB_PATH) as conn:
113
+ for future in concurrent.futures.as_completed(future_to_file):
114
+ file_path, mtime = future_to_file[future]
115
+ try:
116
+ # 10초 이상 걸리는 파일은 강제 중단 (타임아웃)
117
+ content = future.result(timeout=10.0)
118
+
119
+ if content.strip():
120
+ # 트랜잭션: 메타데이터 업데이트 & FTS5 인덱스 재입력
121
+ conn.execute('INSERT OR REPLACE INTO file_meta (file_path, last_modified) VALUES (?, ?)', (file_path, mtime))
122
+ conn.execute('DELETE FROM file_content WHERE file_path = ?', (file_path,))
123
+ conn.execute('INSERT INTO file_content (file_path, content) VALUES (?, ?)', (file_path, content))
124
+ indexed_count += 1
125
+ else:
126
+ skipped_count += 1
127
+ except concurrent.futures.TimeoutError:
128
+ skipped_count += 1
129
+ # 타임아웃된 파일이 매번 다시 시도되어 시스템을 늦추지 않도록 현재 mtime만 갱신 (무시 처리)
130
+ conn.execute('INSERT OR REPLACE INTO file_meta (file_path, last_modified) VALUES (?, ?)', (file_path, mtime))
131
+ except Exception as e:
132
+ print(f"Error processing {file_path}: {e}", file=sys.stderr)
133
+ skipped_count += 1
134
+
135
+ return {
136
+ "status": "success",
137
+ "indexed_count": indexed_count,
138
+ "skipped_count": skipped_count,
139
+ "message": f"{indexed_count}개 파일 인덱싱 완료, {skipped_count}개 스킵(타임아웃/오류)."
140
+ }
141
+
142
+
143
+ @mcp.tool()
144
+ def search_indexed_contents(keyword: str) -> list[dict]:
145
+ """
146
+ [핵심 검색 도구] 인덱싱된 내용을 기반으로 키워드가 포함된 파일과 매칭된 문맥(스니펫)을 즉시 검색합니다.
147
+ 주의: 이 도구를 사용하기 전에 update_file_index가 실행되어 있어야 가장 정확합니다.
148
+ """
149
+ init_db()
150
+ results = []
151
+
152
+ try:
153
+ with sqlite3.connect(DB_PATH) as conn:
154
+ cursor = conn.cursor()
155
+ # snippet을 이용한 매칭 문장 추출 및 관련도 랭킹 정렬
156
+ safe_keyword = f'"{keyword}"'
157
+ query = '''
158
+ SELECT
159
+ file_path,
160
+ snippet(file_content, 1, '[', ']', '...', 15) as context_snippet
161
+ FROM file_content
162
+ WHERE content MATCH ?
163
+ ORDER BY rank
164
+ LIMIT 15
165
+ '''
166
+ cursor.execute(query, (safe_keyword,))
167
+
168
+ for row in cursor.fetchall():
169
+ file_path, context = row
170
+ results.append({
171
+ "file_name": os.path.basename(file_path),
172
+ "file_path": file_path,
173
+ "matched_context": context # 검색어가 포함된 핵심 문장
174
+ })
175
+ except Exception as e:
176
+ print(f"DB search error: {e}", file=sys.stderr)
177
+ return [{"error": f"DB 검색 중 오류 발생 (먼저 인덱싱을 진행해주세요): {str(e)}"}]
178
+
179
+ return results if results else [{"message": "해당 키워드를 포함하는 문서를 찾을 수 없습니다."}]
180
+
181
+
182
+ @mcp.tool()
183
+ def search_local_files(directory: str = None, filename_keyword: str = None, extensions: list[str] = None, days_ago: int = None) -> list[dict]:
184
+ """파일명, 확장자, 수정 날짜를 기준으로 파일을 검색합니다."""
185
+ search_dir = directory or get_default_dir()
186
+ results = []
187
+
188
+ cutoff_time = None
189
+ if days_ago is not None:
190
+ cutoff_time = datetime.now() - timedelta(days=days_ago)
191
+
192
+ try:
193
+ for root, _, files in os.walk(search_dir):
194
+ for file in files:
195
+ if filename_keyword and filename_keyword.lower() not in file.lower():
196
+ continue
197
+ if extensions:
198
+ ext = os.path.splitext(file)[1].lower()
199
+ if ext not in [e.lower() for e in extensions]:
200
+ continue
201
+
202
+ file_path = os.path.join(root, file)
203
+
204
+ try:
205
+ mtime = datetime.fromtimestamp(os.path.getmtime(file_path))
206
+ if cutoff_time and mtime < cutoff_time:
207
+ continue
208
+
209
+ results.append({
210
+ "file_name": file,
211
+ "file_path": file_path,
212
+ "modified_date": mtime.strftime("%Y-%m-%d %H:%M:%S"),
213
+ "size_mb": round(os.path.getsize(file_path) / (1024 * 1024), 2)
214
+ })
215
+ except OSError:
216
+ continue
217
+ except Exception as e:
218
+ return [{"error": f"디렉토리 접근 중 오류 발생: {str(e)}"}]
219
+
220
+ return sorted(results, key=lambda x: x["modified_date"], reverse=True)
221
+
222
+
223
+ @mcp.tool()
224
+ def analyze_disk_usage(directory: str = None, top_n: int = 10) -> list[dict]:
225
+ """특정 디렉토리 내의 하위 폴더와 파일들의 용량을 분석하여 가장 큰 항목들을 반환합니다."""
226
+ search_dir = directory or get_default_dir()
227
+ usage_data = []
228
+
229
+ try:
230
+ for item in os.listdir(search_dir):
231
+ item_path = os.path.join(search_dir, item)
232
+ total_size = 0
233
+
234
+ try:
235
+ if os.path.isfile(item_path):
236
+ total_size = os.path.getsize(item_path)
237
+ elif os.path.isdir(item_path):
238
+ for root, _, files in os.walk(item_path):
239
+ for f in files:
240
+ f_path = os.path.join(root, f)
241
+ if not os.path.islink(f_path):
242
+ total_size += os.path.getsize(f_path)
243
+
244
+ usage_data.append({
245
+ "name": item,
246
+ "path": item_path,
247
+ "is_dir": os.path.isdir(item_path),
248
+ "size_mb": round(total_size / (1024 * 1024), 2)
249
+ })
250
+ except OSError:
251
+ continue
252
+ except Exception as e:
253
+ return [{"error": f"분석 중 오류 발생: {str(e)}"}]
254
+
255
+ return sorted(usage_data, key=lambda x: x["size_mb"], reverse=True)[:top_n]
256
+
257
+
258
+ def main() -> None:
259
+ # 수정점: 클라이언트의 JSON 파싱을 방해하지 않도록 stderr로 출력 방향 설정
260
+ print("Starting Advanced Local File Explorer MCP server...", file=sys.stderr)
261
+ mcp.run(transport='stdio')
262
+
263
+ if __name__ == "__main__":
264
+ main()