Hyeonseo commited on
Commit
168aca5
·
0 Parent(s):

Convert MCP server submodules to regular directories

Browse files
Files changed (10) hide show
  1. .gitattributes +35 -0
  2. README.md +13 -0
  3. adapters.py +48 -0
  4. app.py +152 -0
  5. configs/defaults.yaml +10 -0
  6. pyproject.toml +13 -0
  7. requirements.txt +11 -0
  8. services.py +236 -0
  9. setting.py +62 -0
  10. tools.py +50 -0
.gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
README.md ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Traslation File Explorer
3
+ emoji: ⚡
4
+ colorFrom: red
5
+ colorTo: pink
6
+ sdk: gradio
7
+ sdk_version: 5.49.1
8
+ app_file: app.py
9
+ pinned: false
10
+ license: mit
11
+ ---
12
+
13
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
adapters.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Dict, List
4
+
5
+ import requests
6
+
7
+ from setting import SETTINGS
8
+
9
+
10
+ def _build_auth_headers() -> Dict[str, str]:
11
+ """
12
+ GitHub 호출용 Authorization 헤더 생성.
13
+ - 우선순위: SETTINGS.github_token → (fallback) 환경변수 GITHUB_TOKEN
14
+ """
15
+ token = SETTINGS.github_token
16
+ if not token:
17
+ # 환경변수 직접 조회
18
+ import os
19
+ token = os.environ.get("GITHUB_TOKEN", "")
20
+
21
+ if not token:
22
+ return {}
23
+ return {"Authorization": f"token {token}"}
24
+
25
+
26
+ def fetch_document_paths(api_url: str) -> List[str]:
27
+ """
28
+ GitHub git/trees API에서 blob 경로 목록만 추출.
29
+
30
+ Parameters
31
+ ----------
32
+ api_url : str
33
+ 예: https://api.github.com/repos/huggingface/transformers/git/trees/main?recursive=1
34
+ """
35
+ response = requests.get(
36
+ api_url,
37
+ headers=_build_auth_headers(),
38
+ timeout=SETTINGS.request_timeout_seconds,
39
+ )
40
+
41
+ if response.status_code == 403 and "rate limit" in response.text.lower():
42
+ raise RuntimeError(
43
+ "GitHub API rate limit exceeded. Provide a GITHUB_TOKEN to continue."
44
+ )
45
+
46
+ response.raise_for_status()
47
+ tree = response.json().get("tree", [])
48
+ return [item["path"] for item in tree if item.get("type") == "blob"]
app.py ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import os
5
+
6
+ import gradio as gr
7
+
8
+ from services import get_available_projects, LANGUAGE_CHOICES
9
+ from tools import list_projects, search_files, list_missing_files
10
+ from setting import SETTINGS
11
+
12
+
13
+ def ensure_mcp_support() -> None:
14
+ """Verify that ``gradio[mcp]`` is installed and enable the MCP server flag."""
15
+ try:
16
+ import gradio.mcp # noqa: F401
17
+ except ImportError as exc: # pragma: no cover - runtime guard
18
+ raise RuntimeError("Install gradio[mcp] before launching this module.") from exc
19
+ os.environ.setdefault("GRADIO_MCP_SERVER", "true")
20
+
21
+
22
+ def build_demo() -> gr.Blocks:
23
+ """Create a lightweight Gradio Blocks UI for exercising the MCP tools."""
24
+ projects = get_available_projects()
25
+ languages = LANGUAGE_CHOICES[:]
26
+
27
+ with gr.Blocks(title=SETTINGS.ui_title) as demo:
28
+ gr.Markdown("# Translation MCP Server\nTry the MCP tools exposed below.")
29
+
30
+ # --- 1) Project catalog ---
31
+ with gr.Tab("Project catalog"):
32
+ catalog_output = gr.JSON(label="catalog")
33
+ gr.Button("Fetch").click(
34
+ fn=list_projects,
35
+ inputs=[], # 인자 없음
36
+ outputs=catalog_output,
37
+ api_name="translation_project_catalog",
38
+ )
39
+
40
+ # --- 2) File search (report + candidates) ---
41
+ with gr.Tab("File search"):
42
+ project_input = gr.Dropdown(
43
+ choices=projects,
44
+ label="Project",
45
+ value=projects[0] if projects else "",
46
+ )
47
+ lang_input = gr.Dropdown(
48
+ choices=languages,
49
+ label="Language",
50
+ value=SETTINGS.default_language,
51
+ )
52
+ limit_input = gr.Number(
53
+ label="Limit",
54
+ value=SETTINGS.default_limit,
55
+ minimum=1,
56
+ )
57
+ include_report = gr.Checkbox(
58
+ label="Include status report",
59
+ value=True,
60
+ )
61
+
62
+ search_output = gr.JSON(label="search result")
63
+ gr.Button("Search").click(
64
+ fn=search_files,
65
+ inputs=[project_input, lang_input, limit_input, include_report],
66
+ outputs=search_output,
67
+ api_name="translation_file_search",
68
+ )
69
+
70
+ # --- 3) Missing docs only ---
71
+ with gr.Tab("Missing docs"):
72
+ missing_project = gr.Dropdown(
73
+ choices=projects,
74
+ label="Project",
75
+ value=projects[0] if projects else "",
76
+ )
77
+ missing_lang = gr.Dropdown(
78
+ choices=languages,
79
+ label="Language",
80
+ value=SETTINGS.default_language,
81
+ )
82
+ missing_limit = gr.Number(
83
+ label="Limit",
84
+ value=max(SETTINGS.default_limit, 20),
85
+ minimum=1,
86
+ )
87
+
88
+ missing_output = gr.JSON(label="missing files")
89
+ gr.Button("List missing").click(
90
+ fn=list_missing_files,
91
+ inputs=[missing_project, missing_lang, missing_limit],
92
+ outputs=missing_output,
93
+ api_name="translation_missing_list",
94
+ )
95
+
96
+ return demo
97
+
98
+
99
+ def _parse_args(argv=None) -> argparse.Namespace:
100
+ """Parse CLI arguments used for local or Space deployments."""
101
+ parser = argparse.ArgumentParser(description="Launch the translation MCP demo.")
102
+
103
+ parser.add_argument(
104
+ "--as-space",
105
+ action="store_true",
106
+ help="Use Hugging Face Space defaults.",
107
+ )
108
+ parser.add_argument(
109
+ "--share",
110
+ action="store_true",
111
+ help="Create a public share link.",
112
+ )
113
+ parser.add_argument(
114
+ "--no-queue",
115
+ dest="queue",
116
+ action="store_false",
117
+ help="Disable the request queue.",
118
+ )
119
+ parser.set_defaults(queue=True)
120
+
121
+ return parser.parse_args(argv)
122
+
123
+
124
+ def main(argv=None) -> None:
125
+ """Launch the Gradio app with MCP server support enabled."""
126
+ args = _parse_args(argv)
127
+
128
+ ensure_mcp_support()
129
+
130
+ launch_kwargs = {"mcp_server": True}
131
+
132
+ if args.as_space or os.environ.get("SPACE_ID"):
133
+ launch_kwargs.update(
134
+ {
135
+ "server_name": "0.0.0.0",
136
+ "server_port": int(os.environ.get("PORT", "7860")),
137
+ "show_api": False,
138
+ }
139
+ )
140
+ else:
141
+ launch_kwargs["show_api"] = True
142
+
143
+ if args.share:
144
+ launch_kwargs["share"] = True
145
+
146
+ demo = build_demo()
147
+ app = demo.queue() if args.queue else demo
148
+ app.launch(**launch_kwargs)
149
+
150
+
151
+ if __name__ == "__main__": # pragma: no cover - manual execution helper
152
+ main()
configs/defaults.yaml ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ github:
2
+ token: "" # 기본값: 환경변수 GITHUB_TOKEN 사용 권장
3
+ request_timeout_seconds: 30
4
+
5
+ translation:
6
+ default_language: "ko" # 기본 타겟 언어
7
+ default_limit: 5 # 기본 검색/누락 파일 개수
8
+
9
+ ui:
10
+ title: "Translation Docs Search MCP Server"
pyproject.toml ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "translation-file-explorer-mcp"
3
+ version = "0.1.0"
4
+ requires-python = ">=3.10"
5
+ dependencies = [
6
+ "gradio[mcp]>=5.33.0",
7
+ "pydantic>=2.7.0",
8
+ "requests>=2.31.0",
9
+ "pyyaml>=6.0.1",
10
+ ]
11
+
12
+ [tool.ruff]
13
+ line-length = 100
requirements.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ gradio[mcp]==5.33.0
2
+ requests
3
+ pydantic
4
+ langchain-anthropic
5
+ python-dotenv
6
+ langchain
7
+ PyGithub
8
+ langchain-core
9
+ langchain-community
10
+ boto3
11
+ PyYAML
services.py ADDED
@@ -0,0 +1,236 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from typing import Any, Dict, Iterable, List, Tuple
6
+
7
+ from adapters import fetch_document_paths
8
+ from setting import SETTINGS
9
+
10
+
11
+ # Gradio / UI 에 노출할 언어 선택지
12
+ LANGUAGE_CHOICES: List[str] = [
13
+ "ko",
14
+ "ja",
15
+ "zh",
16
+ "fr",
17
+ "de",
18
+ ]
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class Project:
23
+ """Store the minimum metadata required for documentation lookups."""
24
+
25
+ slug: str
26
+ name: str
27
+ repo_url: str
28
+ docs_path: str
29
+ tree_api_url: str
30
+
31
+ @property
32
+ def repo_path(self) -> str:
33
+ """Return the ``owner/repo`` identifier for GitHub API requests."""
34
+ return self.repo_url.replace("https://github.com/", "")
35
+
36
+
37
+ # 지원 프로젝트 정의
38
+ PROJECTS: Dict[str, Project] = {
39
+ "transformers": Project(
40
+ slug="transformers",
41
+ name="Transformers",
42
+ repo_url="https://github.com/huggingface/transformers",
43
+ docs_path="docs/source",
44
+ tree_api_url=(
45
+ "https://api.github.com/repos/huggingface/transformers/git/trees/main?recursive=1"
46
+ ),
47
+ ),
48
+ "smolagents": Project(
49
+ slug="smolagents",
50
+ name="SmolAgents",
51
+ repo_url="https://github.com/huggingface/smolagents",
52
+ docs_path="docs/source",
53
+ tree_api_url=(
54
+ "https://api.github.com/repos/huggingface/smolagents/git/trees/main?recursive=1"
55
+ ),
56
+ ),
57
+ }
58
+
59
+
60
+ def get_available_projects() -> List[str]:
61
+ """Return the list of project slugs supported by this module."""
62
+ return sorted(PROJECTS.keys())
63
+
64
+
65
+ def _iter_english_docs(all_docs: Iterable[str], docs_root: str) -> Iterable[Path]:
66
+ """Yield English documentation files as ``Path`` objects."""
67
+ english_root = Path(docs_root) / "en"
68
+
69
+ for doc_path in all_docs:
70
+ if not doc_path.endswith(".md"):
71
+ continue
72
+
73
+ path = Path(doc_path)
74
+ try:
75
+ # en/ 아래에 있는지 필터링
76
+ path.relative_to(english_root)
77
+ except ValueError:
78
+ continue
79
+
80
+ yield path
81
+
82
+
83
+ def _compute_missing_translations(
84
+ project_key: str,
85
+ language: str,
86
+ limit: int,
87
+ ) -> Tuple[str, List[str], Project]:
88
+ """
89
+ 영어 기준으로 누락 번역 파일을 계산하고,
90
+ 마크다운 요약 리포트 + 누락 경로 리스트 + Project 메타데이터를 반환.
91
+ """
92
+ project = PROJECTS[project_key]
93
+
94
+ all_paths = fetch_document_paths(project.tree_api_url)
95
+ english_docs = list(_iter_english_docs(all_paths, project.docs_path))
96
+ english_total = len(english_docs)
97
+
98
+ missing: List[str] = []
99
+ docs_set = set(all_paths)
100
+
101
+ for english_doc in english_docs:
102
+ relative = english_doc.relative_to(Path(project.docs_path) / "en")
103
+ translated_path = str(Path(project.docs_path) / language / relative)
104
+
105
+ if translated_path not in docs_set:
106
+ # 누락된 경우: 기준은 영어 경로(en/...)
107
+ missing.append(str(english_doc))
108
+ if len(missing) >= limit:
109
+ break
110
+
111
+ missing_count = len(missing)
112
+ percentage = (missing_count / english_total * 100) if english_total else 0.0
113
+
114
+ report = (
115
+ "| Item | Count | Percentage |\n"
116
+ "|------|-------|------------|\n"
117
+ f"| English docs | {english_total} | - |\n"
118
+ f"| Missing translations | {missing_count} | {percentage:.2f}% |"
119
+ )
120
+
121
+ return report, missing, project
122
+
123
+
124
+ def build_project_catalog(default: str | None) -> Dict[str, Any]:
125
+ """Build the project catalog payload (API-neutral, pure logic)."""
126
+ slugs = get_available_projects()
127
+ default = default if default in slugs else None
128
+
129
+ return {
130
+ "type": "translation.project_list",
131
+ "projects": [
132
+ {
133
+ "slug": slug,
134
+ "display_name": PROJECTS[slug].name,
135
+ "repo_url": PROJECTS[slug].repo_url,
136
+ "docs_path": PROJECTS[slug].docs_path,
137
+ }
138
+ for slug in slugs
139
+ ],
140
+ "default_project": default,
141
+ "total_projects": len(slugs),
142
+ }
143
+
144
+
145
+ def build_search_response(
146
+ project: str,
147
+ lang: str,
148
+ limit: int,
149
+ include_status_report: bool,
150
+ ) -> Dict[str, Any]:
151
+ """
152
+ 누락 번역 파일 후보 + (선택) 상태 리포트를 포함한 검색 응답.
153
+ MCP / Gradio 에서 사용 가능한 JSON 형태.
154
+ """
155
+ project = project.strip()
156
+ lang = lang.strip()
157
+ limit = max(1, int(limit))
158
+
159
+ project_config = PROJECTS[project]
160
+
161
+ status_report, candidate_paths, project_config = _compute_missing_translations(
162
+ project_key=project,
163
+ language=lang,
164
+ limit=limit,
165
+ )
166
+
167
+ repo_url = project_config.repo_url.rstrip("/")
168
+
169
+ return {
170
+ "type": "translation.search.response",
171
+ "request": {
172
+ "type": "translation.search.request",
173
+ "project": project,
174
+ "target_language": lang,
175
+ "limit": limit,
176
+ "include_status_report": include_status_report,
177
+ },
178
+ "files": [
179
+ {
180
+ "rank": index,
181
+ "path": path,
182
+ "repo_url": f"{repo_url}/blob/main/{path}",
183
+ "metadata": {
184
+ "project": project,
185
+ "target_language": lang,
186
+ "docs_path": project_config.docs_path,
187
+ },
188
+ }
189
+ for index, path in enumerate(candidate_paths, start=1)
190
+ ],
191
+ "total_candidates": len(candidate_paths),
192
+ "status_report": status_report if include_status_report else None,
193
+ }
194
+
195
+
196
+ def build_missing_list_response(
197
+ project: str,
198
+ lang: str,
199
+ limit: int,
200
+ ) -> Dict[str, Any]:
201
+ """
202
+ 누락 번역 파일 목록만 제공하는 응답(JSON).
203
+ """
204
+ project = project.strip()
205
+ lang = lang.strip()
206
+ limit_int = max(1, int(limit))
207
+
208
+ status_report, missing_paths, project_config = _compute_missing_translations(
209
+ project_key=project,
210
+ language=lang,
211
+ limit=limit_int,
212
+ )
213
+
214
+ repo_url = project_config.repo_url.rstrip("/")
215
+
216
+ return {
217
+ "type": "translation.missing_list",
218
+ "project": project,
219
+ "target_language": lang,
220
+ "limit": limit_int,
221
+ "count": len(missing_paths),
222
+ "files": [
223
+ {
224
+ "rank": index,
225
+ "path": path,
226
+ "repo_url": f"{repo_url}/blob/main/{path}",
227
+ "metadata": {
228
+ "project": project,
229
+ "target_language": lang,
230
+ "docs_path": project_config.docs_path,
231
+ },
232
+ }
233
+ for index, path in enumerate(missing_paths, start=1)
234
+ ],
235
+ "status_report": status_report, # 필요 없다면 제거 가능
236
+ }
setting.py ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from typing import Any, Dict
6
+
7
+ import os
8
+
9
+ try:
10
+ import yaml # type: ignore
11
+ except Exception:
12
+ yaml = None
13
+
14
+
15
+ @dataclass
16
+ class AppSettings:
17
+ github_token: str = ""
18
+ request_timeout_seconds: int = 30
19
+ default_language: str = "ko"
20
+ default_limit: int = 5
21
+ ui_title: str = "Translation MCP Server"
22
+
23
+
24
+ def _load_yaml(path: Path) -> Dict[str, Any]:
25
+ if not path.is_file():
26
+ return {}
27
+ if yaml is None:
28
+ return {}
29
+ with path.open("r", encoding="utf-8") as f:
30
+ data = yaml.safe_load(f) or {}
31
+ return data if isinstance(data, dict) else {}
32
+
33
+
34
+ def load_settings(config_path: str = "configs/default.yaml") -> AppSettings:
35
+ cfg = _load_yaml(Path(config_path))
36
+
37
+ github_cfg = cfg.get("github", {}) if isinstance(cfg.get("github"), dict) else {}
38
+ trans_cfg = cfg.get("translation", {}) if isinstance(cfg.get("translation"), dict) else {}
39
+ ui_cfg = cfg.get("ui", {}) if isinstance(cfg.get("ui"), dict) else {}
40
+
41
+ # ENV > YAML
42
+ github_token = os.getenv("GITHUB_TOKEN", github_cfg.get("token", ""))
43
+ request_timeout_seconds = int(
44
+ os.getenv("REQUEST_TIMEOUT_SECONDS", github_cfg.get("request_timeout_seconds", 30))
45
+ )
46
+ default_language = os.getenv("DEFAULT_LANGUAGE", trans_cfg.get("default_language", "ko"))
47
+ default_limit = int(
48
+ os.getenv("DEFAULT_LIMIT", trans_cfg.get("default_limit", 5))
49
+ )
50
+ ui_title = ui_cfg.get("title", "Translation MCP Server")
51
+
52
+ return AppSettings(
53
+ github_token=github_token,
54
+ request_timeout_seconds=request_timeout_seconds,
55
+ default_language=default_language,
56
+ default_limit=default_limit,
57
+ ui_title=ui_title,
58
+ )
59
+
60
+
61
+ # 전역 설정 인스턴스
62
+ SETTINGS: AppSettings = load_settings()
tools.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict
4
+
5
+ from services import (
6
+ build_project_catalog,
7
+ build_search_response,
8
+ build_missing_list_response,
9
+ )
10
+
11
+
12
+ def list_projects() -> Dict[str, Any]:
13
+ """
14
+ Gradio + MCP에서 사용되는 'translation_project_catalog' 엔드포인트.
15
+ 입력값 없이 전체 프로젝트 카탈로그를 반환한다.
16
+ """
17
+ return build_project_catalog(default=None)
18
+
19
+
20
+ def search_files(
21
+ project: str,
22
+ lang: str,
23
+ limit: float | int,
24
+ include_status_report: bool,
25
+ ) -> Dict[str, Any]:
26
+ """
27
+ Gradio + MCP에서 사용되는 'translation_file_search' 엔드포인트.
28
+ """
29
+ return build_search_response(
30
+ project=project,
31
+ lang=lang,
32
+ limit=int(limit or 1),
33
+ include_status_report=bool(include_status_report),
34
+ )
35
+
36
+
37
+ def list_missing_files(
38
+ project: str,
39
+ lang: str,
40
+ limit: float | int,
41
+ ) -> Dict[str, Any]:
42
+ """
43
+ Gradio + MCP에서 사용되는 'translation_missing_list' 엔드포인트.
44
+ 누락 파일 리스트만 반환.
45
+ """
46
+ return build_missing_list_response(
47
+ project=project,
48
+ lang=lang,
49
+ limit=int(limit or 1),
50
+ )