hossam 0.3.19__py3-none-any.whl → 0.4__py3-none-any.whl
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.
- hossam/__init__.py +19 -22
- hossam/data_loader.py +16 -10
- hossam/hs_classroom.py +69 -44
- hossam/hs_gis.py +10 -6
- hossam/hs_plot.py +153 -150
- hossam/hs_prep.py +95 -85
- hossam/hs_stats.py +426 -548
- hossam/hs_timeserise.py +161 -152
- hossam/hs_util.py +44 -17
- {hossam-0.3.19.dist-info → hossam-0.4.dist-info}/METADATA +6 -107
- hossam-0.4.dist-info/RECORD +16 -0
- hossam/mcp/__init__.py +0 -12
- hossam/mcp/hs_classroom.py +0 -22
- hossam/mcp/hs_gis.py +0 -30
- hossam/mcp/hs_plot.py +0 -53
- hossam/mcp/hs_prep.py +0 -61
- hossam/mcp/hs_stats.py +0 -25
- hossam/mcp/hs_timeserise.py +0 -22
- hossam/mcp/hs_util.py +0 -30
- hossam/mcp/loader.py +0 -29
- hossam/mcp/server.py +0 -675
- hossam-0.3.19.dist-info/RECORD +0 -27
- hossam-0.3.19.dist-info/entry_points.txt +0 -2
- {hossam-0.3.19.dist-info → hossam-0.4.dist-info}/WHEEL +0 -0
- {hossam-0.3.19.dist-info → hossam-0.4.dist-info}/licenses/LICENSE +0 -0
- {hossam-0.3.19.dist-info → hossam-0.4.dist-info}/top_level.txt +0 -0
hossam/mcp/server.py
DELETED
|
@@ -1,675 +0,0 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
"""
|
|
3
|
-
Hossam MCP Server - VSCode/Copilot Compatible
|
|
4
|
-
|
|
5
|
-
표준 MCP(Model Context Protocol) 호환 서버입니다.
|
|
6
|
-
- StdIO (표준입출력) 기반 JSON 라인 프로토콜
|
|
7
|
-
- VSCode Copilot Chat, Cline, Cursor 등과 호환
|
|
8
|
-
- 모든 hossam 도구를 MCP tool로 등록
|
|
9
|
-
|
|
10
|
-
실행:
|
|
11
|
-
python -m hossam.mcp.server
|
|
12
|
-
또는
|
|
13
|
-
hossam-mcp (CLI 엔트리포인트)
|
|
14
|
-
"""
|
|
15
|
-
import sys
|
|
16
|
-
import os
|
|
17
|
-
import json
|
|
18
|
-
import logging
|
|
19
|
-
import inspect
|
|
20
|
-
import time
|
|
21
|
-
from typing import Any, Callable, Dict, Optional
|
|
22
|
-
import contextlib
|
|
23
|
-
import io
|
|
24
|
-
from typing import List, Tuple
|
|
25
|
-
|
|
26
|
-
# 로깅 설정 (stderr로 출력, stdout은 MCP 프로토콜 전용)
|
|
27
|
-
logging.basicConfig(
|
|
28
|
-
level=logging.INFO, # INFO로 변경하여 요청/응답 로그 표시
|
|
29
|
-
format="[%(asctime)s] [hossam-mcp] %(levelname)s - %(message)s",
|
|
30
|
-
datefmt="%H:%M:%S",
|
|
31
|
-
stream=sys.stderr,
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
try:
|
|
35
|
-
import pandas as pd
|
|
36
|
-
from pandas import DataFrame
|
|
37
|
-
except Exception:
|
|
38
|
-
pd = None
|
|
39
|
-
DataFrame = Any
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
class HossamMCP:
|
|
43
|
-
"""경량 MCP 서버 구현 클래스.
|
|
44
|
-
|
|
45
|
-
이 클래스는 MCP(모델 컨텍스트 프로토콜)에서 사용할 도구를 등록하고
|
|
46
|
-
조회/호출하는 기능을 제공합니다.
|
|
47
|
-
|
|
48
|
-
Attributes:
|
|
49
|
-
name (str): 서버/네임스페이스 이름.
|
|
50
|
-
_tools (Dict[str, Dict[str, Any]]): 등록된 도구 메타데이터.
|
|
51
|
-
"""
|
|
52
|
-
|
|
53
|
-
def __init__(self, name: str = "hossam"):
|
|
54
|
-
"""초기화.
|
|
55
|
-
|
|
56
|
-
Args:
|
|
57
|
-
name (str): MCP 서버 이름.
|
|
58
|
-
"""
|
|
59
|
-
self.name = name
|
|
60
|
-
self._tools: Dict[str, Dict[str, Any]] = {}
|
|
61
|
-
|
|
62
|
-
def tool(self, name: Optional[str] = None, description: str = ""):
|
|
63
|
-
"""도구 등록용 데코레이터.
|
|
64
|
-
|
|
65
|
-
MCP에서 호출 가능한 함수를 등록합니다. 등록 시 도구명은 `hs_` 접두사로
|
|
66
|
-
정규화되며, 시그니처와 파라미터 메타데이터를 함께 저장합니다.
|
|
67
|
-
|
|
68
|
-
Args:
|
|
69
|
-
name (Optional[str]): 명시적 도구명. 미지정 시 함수명을 사용.
|
|
70
|
-
description (str): 도구 설명. 미지정 시 함수 docstring 1행.
|
|
71
|
-
|
|
72
|
-
Returns:
|
|
73
|
-
Callable: 원본 함수 데코레이터.
|
|
74
|
-
"""
|
|
75
|
-
|
|
76
|
-
def decorator(fn: Callable[..., Any]):
|
|
77
|
-
tool_name = name or fn.__name__
|
|
78
|
-
if not tool_name.startswith("hs_"):
|
|
79
|
-
tool_name = f"hs_{tool_name}"
|
|
80
|
-
|
|
81
|
-
sig = inspect.signature(fn)
|
|
82
|
-
doc = (description or fn.__doc__ or "No description").split("\n")[0]
|
|
83
|
-
|
|
84
|
-
self._tools[tool_name] = {
|
|
85
|
-
"fn": fn,
|
|
86
|
-
"description": doc,
|
|
87
|
-
"doc": fn.__doc__ or "",
|
|
88
|
-
"module": getattr(fn, "__module__", None),
|
|
89
|
-
"signature": str(sig),
|
|
90
|
-
"params": {
|
|
91
|
-
pname: {
|
|
92
|
-
"kind": str(param.kind),
|
|
93
|
-
"required": param.default is inspect._empty,
|
|
94
|
-
}
|
|
95
|
-
for pname, param in sig.parameters.items()
|
|
96
|
-
},
|
|
97
|
-
"returns": "python_code",
|
|
98
|
-
"mode": "codegen_only",
|
|
99
|
-
}
|
|
100
|
-
return fn
|
|
101
|
-
|
|
102
|
-
return decorator
|
|
103
|
-
|
|
104
|
-
def list_tools(self) -> Dict[str, Dict[str, Any]]:
|
|
105
|
-
"""등록된 도구 명세를 반환합니다.
|
|
106
|
-
|
|
107
|
-
Returns:
|
|
108
|
-
Dict[str, Dict[str, Any]]: 도구 이름별 설명/시그니처/파라미터/리턴 타입.
|
|
109
|
-
"""
|
|
110
|
-
return {
|
|
111
|
-
name: {
|
|
112
|
-
"description": spec["description"],
|
|
113
|
-
"signature": spec["signature"],
|
|
114
|
-
"params": spec["params"],
|
|
115
|
-
"returns": spec["returns"],
|
|
116
|
-
"mode": spec.get("mode", "codegen_only"),
|
|
117
|
-
}
|
|
118
|
-
for name, spec in self._tools.items()
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
def get_tool_info(self, name: str) -> Optional[Dict[str, Any]]:
|
|
122
|
-
"""특정 도구의 상세 정보를 조회합니다.
|
|
123
|
-
|
|
124
|
-
Args:
|
|
125
|
-
name (str): 도구명.
|
|
126
|
-
|
|
127
|
-
Returns:
|
|
128
|
-
Optional[Dict[str, Any]]: 도구 메타데이터 또는 None.
|
|
129
|
-
"""
|
|
130
|
-
return self._tools.get(name)
|
|
131
|
-
|
|
132
|
-
def call(self, tool: str, **kwargs) -> Any:
|
|
133
|
-
"""도구 호출 또는 코드 생성.
|
|
134
|
-
|
|
135
|
-
기본 동작은 코드 생성(`mode='code'`). 실행이 필요하면 `mode='run'`을 지정하거나
|
|
136
|
-
`run/execute/result` 플래그를 사용합니다.
|
|
137
|
-
|
|
138
|
-
Args:
|
|
139
|
-
tool (str): 도구명.
|
|
140
|
-
**kwargs: 도구 인자 및 실행 모드 지정.
|
|
141
|
-
|
|
142
|
-
Returns:
|
|
143
|
-
Any: 코드 문자열 또는 실제 실행 결과.
|
|
144
|
-
"""
|
|
145
|
-
if tool not in self._tools:
|
|
146
|
-
raise KeyError(f"Unknown tool: {tool}")
|
|
147
|
-
|
|
148
|
-
meta = self._tools[tool]
|
|
149
|
-
mode = kwargs.pop("mode", None) or kwargs.pop("return", None)
|
|
150
|
-
|
|
151
|
-
# 실행/코드 플래그 해석
|
|
152
|
-
run_flag = kwargs.pop("run", None) or kwargs.pop("execute", None) or kwargs.pop("result", None)
|
|
153
|
-
code_flag = kwargs.pop("code", None) or kwargs.pop("code_only", None)
|
|
154
|
-
|
|
155
|
-
if mode is None:
|
|
156
|
-
mode = "run" if run_flag else "code"
|
|
157
|
-
|
|
158
|
-
mode = str(mode).lower() if mode else "code"
|
|
159
|
-
|
|
160
|
-
if mode == "code":
|
|
161
|
-
return _generate_code(tool, meta, kwargs)
|
|
162
|
-
|
|
163
|
-
fn = meta["fn"]
|
|
164
|
-
return fn(**kwargs)
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
def _df_from_any(obj: Any) -> Any:
|
|
170
|
-
"""입력 객체를 `pandas.DataFrame`으로 변환합니다.
|
|
171
|
-
|
|
172
|
-
CSV/XLSX 경로 문자열과 시퀀스/매핑 객체를 지원합니다.
|
|
173
|
-
|
|
174
|
-
Args:
|
|
175
|
-
obj (Any): 입력 데이터 또는 파일 경로.
|
|
176
|
-
|
|
177
|
-
Returns:
|
|
178
|
-
DataFrame: 변환된 데이터프레임.
|
|
179
|
-
|
|
180
|
-
Raises:
|
|
181
|
-
RuntimeError: pandas 미설치 시.
|
|
182
|
-
ValueError: 지원하지 않는 경로 또는 변환 실패.
|
|
183
|
-
"""
|
|
184
|
-
if pd is None:
|
|
185
|
-
raise RuntimeError("pandas 필요: pip install pandas")
|
|
186
|
-
|
|
187
|
-
if isinstance(obj, pd.DataFrame):
|
|
188
|
-
return obj
|
|
189
|
-
|
|
190
|
-
if isinstance(obj, str):
|
|
191
|
-
s = obj.lower()
|
|
192
|
-
if s.endswith(".csv"):
|
|
193
|
-
return pd.read_csv(obj)
|
|
194
|
-
if s.endswith(".xlsx"):
|
|
195
|
-
return pd.read_excel(obj)
|
|
196
|
-
raise ValueError("CSV/XLSX 경로만 지원")
|
|
197
|
-
|
|
198
|
-
try:
|
|
199
|
-
return pd.DataFrame(obj)
|
|
200
|
-
except Exception:
|
|
201
|
-
raise ValueError("DataFrame으로 변환 불가")
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
def _serialize(obj: Any) -> Any:
|
|
205
|
-
"""MCP 응답을 위한 직렬화 헬퍼.
|
|
206
|
-
|
|
207
|
-
pandas 객체와 numpy 배열을 JSON 호환 형태로 변환합니다.
|
|
208
|
-
|
|
209
|
-
Args:
|
|
210
|
-
obj (Any): 직렬화 대상.
|
|
211
|
-
|
|
212
|
-
Returns:
|
|
213
|
-
Any: JSON 호환 객체.
|
|
214
|
-
"""
|
|
215
|
-
import numpy as np
|
|
216
|
-
|
|
217
|
-
if pd is not None and isinstance(obj, pd.DataFrame):
|
|
218
|
-
return {
|
|
219
|
-
"index": obj.index.tolist(),
|
|
220
|
-
"columns": obj.columns.tolist(),
|
|
221
|
-
"data": obj.where(pd.notnull(obj), None).values.tolist(),
|
|
222
|
-
}
|
|
223
|
-
if pd is not None and isinstance(obj, pd.Series):
|
|
224
|
-
return {
|
|
225
|
-
"index": obj.index.tolist(),
|
|
226
|
-
"name": obj.name,
|
|
227
|
-
"data": obj.where(pd.notnull(obj), None).tolist(),
|
|
228
|
-
}
|
|
229
|
-
if isinstance(obj, (list, dict, str, int, float, bool)) or obj is None:
|
|
230
|
-
return obj
|
|
231
|
-
if isinstance(obj, np.ndarray):
|
|
232
|
-
return obj.tolist()
|
|
233
|
-
|
|
234
|
-
return str(obj)
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
def _py_repr(val: Any) -> str:
|
|
238
|
-
"""파이썬 리터럴/JSON 문자열로 안전하게 변환합니다.
|
|
239
|
-
|
|
240
|
-
Args:
|
|
241
|
-
val (Any): 값.
|
|
242
|
-
|
|
243
|
-
Returns:
|
|
244
|
-
str: 코드 내에서 사용 가능한 표현 문자열.
|
|
245
|
-
"""
|
|
246
|
-
import json as _json
|
|
247
|
-
if isinstance(val, str):
|
|
248
|
-
return repr(val)
|
|
249
|
-
try:
|
|
250
|
-
return _json.dumps(val, ensure_ascii=False)
|
|
251
|
-
except Exception:
|
|
252
|
-
return repr(val)
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
def _log_request(request_id: Any, method: Optional[str], params: Dict[str, Any]) -> None:
|
|
256
|
-
"""요청 로깅 헬퍼.
|
|
257
|
-
|
|
258
|
-
Args:
|
|
259
|
-
request_id (Any): 요청 ID.
|
|
260
|
-
method (Optional[str]): 메서드 이름.
|
|
261
|
-
params (Dict[str, Any]): 파라미터.
|
|
262
|
-
"""
|
|
263
|
-
logging.info("=" * 80)
|
|
264
|
-
logging.info(f"📥 Request [id: {request_id}]")
|
|
265
|
-
logging.info(f" Method: {method}")
|
|
266
|
-
if params:
|
|
267
|
-
logging.info(f" Params: {str(params)[:200]}...")
|
|
268
|
-
logging.info("=" * 80)
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
def _build_tools_list(mcp: HossamMCP) -> List[Dict[str, Any]]:
|
|
272
|
-
"""도구 목록을 MCP 형식으로 구성합니다.
|
|
273
|
-
|
|
274
|
-
Args:
|
|
275
|
-
mcp (HossamMCP): 서버 인스턴스.
|
|
276
|
-
|
|
277
|
-
Returns:
|
|
278
|
-
List[Dict[str, Any]]: MCP tools/list 응답용 배열.
|
|
279
|
-
"""
|
|
280
|
-
return [
|
|
281
|
-
{
|
|
282
|
-
"name": name,
|
|
283
|
-
"description": spec["description"],
|
|
284
|
-
"inputSchema": {
|
|
285
|
-
"type": "object",
|
|
286
|
-
"properties": {},
|
|
287
|
-
"required": [],
|
|
288
|
-
},
|
|
289
|
-
}
|
|
290
|
-
for name, spec in mcp.list_tools().items()
|
|
291
|
-
]
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
# MCP 상수: 프로토콜/서버 정보
|
|
295
|
-
PROTOCOL_VERSION = "2024-11-05"
|
|
296
|
-
SERVER_NAME = "hossam-mcp"
|
|
297
|
-
SERVER_VERSION = "1.0.0"
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
def _base_module_for_tool(tool: str, meta: Dict[str, Any]) -> Tuple[str, str]:
|
|
301
|
-
"""도구명/메타데이터를 기반으로 import 경로와 함수명을 추정합니다.
|
|
302
|
-
|
|
303
|
-
규칙: `hs_모듈_함수` 형태의 도구명 사용을 권장합니다.
|
|
304
|
-
|
|
305
|
-
Args:
|
|
306
|
-
tool (str): 도구명.
|
|
307
|
-
meta (Dict[str, Any]): 등록 메타데이터.
|
|
308
|
-
|
|
309
|
-
Returns:
|
|
310
|
-
Tuple[str, str]: 베이스 모듈 경로, 함수명.
|
|
311
|
-
"""
|
|
312
|
-
fn = meta.get("fn")
|
|
313
|
-
mod = meta.get("module") or getattr(fn, "__module__", "")
|
|
314
|
-
|
|
315
|
-
# 도구명에서 모듈명과 함수명 파싱
|
|
316
|
-
# 예: hs_util_load_data -> (hs_util, load_data)
|
|
317
|
-
# 예: hs_plot_histplot -> (hs_plot, histplot)
|
|
318
|
-
if tool.startswith("hs_"):
|
|
319
|
-
parts = tool.split("_", 2) # ['hs', 'util', 'load_data']
|
|
320
|
-
if len(parts) >= 3:
|
|
321
|
-
module_name = f"{parts[0]}_{parts[1]}" # hs_util
|
|
322
|
-
func = parts[2] # load_data
|
|
323
|
-
else:
|
|
324
|
-
# 폴백 (구버전 호환)
|
|
325
|
-
module_name = "hs_util"
|
|
326
|
-
func = tool[3:]
|
|
327
|
-
else:
|
|
328
|
-
module_name = "hs_util"
|
|
329
|
-
func = tool
|
|
330
|
-
|
|
331
|
-
if mod.startswith("hossam.mcp."):
|
|
332
|
-
# mcp 래퍼에서 온 경우: 실제 모듈은 hossam.뒤꼬리
|
|
333
|
-
tail = mod.split("hossam.mcp.", 1)[1]
|
|
334
|
-
base_mod = f"hossam.{tail}"
|
|
335
|
-
elif mod.startswith("hossam."):
|
|
336
|
-
base_mod = mod
|
|
337
|
-
func = getattr(fn, "__name__", func)
|
|
338
|
-
else:
|
|
339
|
-
# 폴백: 툴명의 모듈 부분 사용
|
|
340
|
-
base_mod = f"hossam.{module_name[3:]}" # hs_util -> hossam.util (잘못된 경우)
|
|
341
|
-
# 수정: data_loader는 data_loader로
|
|
342
|
-
if module_name == "hs_data":
|
|
343
|
-
base_mod = "hossam.data_loader"
|
|
344
|
-
elif module_name.startswith("hs_"):
|
|
345
|
-
base_mod = f"hossam.{module_name}"
|
|
346
|
-
|
|
347
|
-
return base_mod, func
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
def _generate_code(tool: str, meta: Dict[str, Any], args: Dict[str, Any]) -> str:
|
|
351
|
-
"""도구 호출 예제 파이썬 코드를 생성합니다.
|
|
352
|
-
|
|
353
|
-
DataFrame 인자를 자동으로 적절한 로드 코드로 변환합니다.
|
|
354
|
-
|
|
355
|
-
Args:
|
|
356
|
-
tool (str): 도구명.
|
|
357
|
-
meta (Dict[str, Any]): 도구 메타데이터.
|
|
358
|
-
args (Dict[str, Any]): 호출 인자.
|
|
359
|
-
|
|
360
|
-
Returns:
|
|
361
|
-
str: 실행 가능한 예제 코드 문자열.
|
|
362
|
-
"""
|
|
363
|
-
base_mod, func = _base_module_for_tool(tool, meta)
|
|
364
|
-
|
|
365
|
-
lines: List[str] = []
|
|
366
|
-
|
|
367
|
-
# df 전처리 코드 스니펫 구성
|
|
368
|
-
call_args = []
|
|
369
|
-
for k, v in list(args.items()):
|
|
370
|
-
if k == "df":
|
|
371
|
-
if isinstance(v, str) and v.lower().endswith(".csv"):
|
|
372
|
-
lines.append("import pandas as pd")
|
|
373
|
-
lines.append(f"df = pd.read_csv({repr(v)})")
|
|
374
|
-
call_args.append("df=df")
|
|
375
|
-
elif isinstance(v, str) and v.lower().endswith(".xlsx"):
|
|
376
|
-
lines.append("import pandas as pd")
|
|
377
|
-
lines.append(f"df = pd.read_excel({repr(v)})")
|
|
378
|
-
call_args.append("df=df")
|
|
379
|
-
else:
|
|
380
|
-
lines.append("import pandas as pd")
|
|
381
|
-
lines.append(f"df = pd.DataFrame({_py_repr(v)})")
|
|
382
|
-
call_args.append("df=df")
|
|
383
|
-
args.pop(k, None)
|
|
384
|
-
else:
|
|
385
|
-
call_args.append(f"{k}={_py_repr(v)}")
|
|
386
|
-
|
|
387
|
-
# import 라인
|
|
388
|
-
# 도구 import 라인
|
|
389
|
-
lines.append(f"from {base_mod} import {func}")
|
|
390
|
-
# 호출 라인
|
|
391
|
-
args_str = ", ".join(call_args)
|
|
392
|
-
call_line = f"result = {func}({args_str})" if call_args else f"result = {func}()"
|
|
393
|
-
lines.append(call_line)
|
|
394
|
-
lines.append("print(result)")
|
|
395
|
-
|
|
396
|
-
return "\n".join(lines)
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
@contextlib.contextmanager
|
|
400
|
-
def _suppress_import_stdout():
|
|
401
|
-
"""모듈 임포트 중 stdout 배너 출력 억제.
|
|
402
|
-
|
|
403
|
-
MCP 표준 출력 채널을 보호하기 위해 임포트 중 발생하는 배너/프린트를 차단합니다.
|
|
404
|
-
|
|
405
|
-
Yields:
|
|
406
|
-
None: 컨텍스트 종료 시 stdout 복구.
|
|
407
|
-
"""
|
|
408
|
-
original = sys.stdout
|
|
409
|
-
try:
|
|
410
|
-
sys.stdout = io.StringIO() # 배너를 버립니다
|
|
411
|
-
yield
|
|
412
|
-
finally:
|
|
413
|
-
sys.stdout = original
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
def _register_all(mcp: HossamMCP):
|
|
417
|
-
"""모든 hossam MCP 도구를 서버에 등록합니다.
|
|
418
|
-
|
|
419
|
-
Args:
|
|
420
|
-
mcp (HossamMCP): MCP 서버 인스턴스.
|
|
421
|
-
"""
|
|
422
|
-
with _suppress_import_stdout():
|
|
423
|
-
from . import hs_stats as mcp_stats
|
|
424
|
-
mcp_stats.register(mcp)
|
|
425
|
-
from . import hs_plot as mcp_plot
|
|
426
|
-
mcp_plot.register(mcp)
|
|
427
|
-
from . import hs_prep as mcp_prep
|
|
428
|
-
mcp_prep.register(mcp)
|
|
429
|
-
from . import hs_gis as mcp_gis
|
|
430
|
-
mcp_gis.register(mcp)
|
|
431
|
-
from . import hs_timeserise as mcp_ts
|
|
432
|
-
mcp_ts.register(mcp)
|
|
433
|
-
from . import hs_classroom as mcp_classroom
|
|
434
|
-
mcp_classroom.register(mcp)
|
|
435
|
-
from . import hs_util as mcp_util
|
|
436
|
-
mcp_util.register(mcp)
|
|
437
|
-
# data_loader 공개 함수도 노출
|
|
438
|
-
try:
|
|
439
|
-
from . import loader as mcp_loader
|
|
440
|
-
mcp_loader.register(mcp)
|
|
441
|
-
except Exception:
|
|
442
|
-
# 선택 모듈 실패는 전체 서버 동작에 영향 없도록 무시
|
|
443
|
-
pass
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
def _write_message(obj: Dict[str, Any]):
|
|
448
|
-
"""JSON-RPC 2.0 메시지를 Content-Length 헤더로 프레이밍하여 전송합니다.
|
|
449
|
-
|
|
450
|
-
Args:
|
|
451
|
-
obj (Dict[str, Any]): 전송할 JSON 객체.
|
|
452
|
-
"""
|
|
453
|
-
payload = json.dumps(obj, ensure_ascii=False)
|
|
454
|
-
data = payload.encode("utf-8")
|
|
455
|
-
# 표준 MCP/LSP 스타일 헤더 프레이밍
|
|
456
|
-
sys.stdout.write(f"Content-Length: {len(data)}\r\n\r\n")
|
|
457
|
-
sys.stdout.write(payload)
|
|
458
|
-
sys.stdout.flush()
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
def _send_response(response: Dict[str, Any]):
|
|
462
|
-
"""JSON-RPC 2.0 형식의 MCP 응답을 전송합니다.
|
|
463
|
-
|
|
464
|
-
Args:
|
|
465
|
-
response (Dict[str, Any]): 응답 객체(`jsonrpc`/`id`/`result` 또는 `error`).
|
|
466
|
-
"""
|
|
467
|
-
# 응답 로깅 (stderr)
|
|
468
|
-
if "result" in response:
|
|
469
|
-
result_preview = str(response.get("result", ""))[:80]
|
|
470
|
-
logging.info(f"📤 Response [id: {response.get('id')}] - Result: {result_preview}...")
|
|
471
|
-
elif "error" in response:
|
|
472
|
-
logging.error(f"📤 Response [id: {response.get('id')}] - Error: {response['error']}")
|
|
473
|
-
|
|
474
|
-
_write_message(response)
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
def _send_error(request_id: Any, code: int, message: str):
|
|
478
|
-
"""JSON-RPC 2.0 에러 응답을 전송합니다.
|
|
479
|
-
|
|
480
|
-
Args:
|
|
481
|
-
request_id (Any): 요청 식별자.
|
|
482
|
-
code (int): 에러 코드.
|
|
483
|
-
message (str): 에러 메시지.
|
|
484
|
-
"""
|
|
485
|
-
_send_response({
|
|
486
|
-
"jsonrpc": "2.0",
|
|
487
|
-
"id": request_id,
|
|
488
|
-
"error": {
|
|
489
|
-
"code": code,
|
|
490
|
-
"message": message
|
|
491
|
-
}
|
|
492
|
-
})
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
def _read_json_rpc_message() -> Optional[Dict[str, Any]]:
|
|
496
|
-
"""STDIO에서 JSON-RPC 2.0 메시지를 읽습니다.
|
|
497
|
-
|
|
498
|
-
MCP/LSP 호환을 위해 Content-Length 헤더 프레이밍을 우선 사용하고
|
|
499
|
-
필요 시 라인 기반 폴백을 지원합니다.
|
|
500
|
-
|
|
501
|
-
Returns:
|
|
502
|
-
Optional[Dict[str, Any]]: 파싱된 요청 객체 또는 None.
|
|
503
|
-
"""
|
|
504
|
-
buf = sys.stdin.buffer
|
|
505
|
-
|
|
506
|
-
# 첫 라인 확인: 바로 JSON이면 폴백 처리
|
|
507
|
-
first = buf.readline()
|
|
508
|
-
if not first:
|
|
509
|
-
return None
|
|
510
|
-
first_text = first.decode("utf-8", errors="ignore").strip()
|
|
511
|
-
if first_text.startswith("{"):
|
|
512
|
-
try:
|
|
513
|
-
return json.loads(first_text)
|
|
514
|
-
except Exception:
|
|
515
|
-
return None
|
|
516
|
-
|
|
517
|
-
# 헤더 파싱
|
|
518
|
-
headers: Dict[str, str] = {}
|
|
519
|
-
line = first_text
|
|
520
|
-
while True:
|
|
521
|
-
if not line:
|
|
522
|
-
# 빈 라인: 헤더 종료
|
|
523
|
-
break
|
|
524
|
-
if ":" in line:
|
|
525
|
-
k, v = line.split(":", 1)
|
|
526
|
-
headers[k.strip().lower()] = v.strip()
|
|
527
|
-
# 다음 라인
|
|
528
|
-
nxt = buf.readline()
|
|
529
|
-
if not nxt:
|
|
530
|
-
return None
|
|
531
|
-
line = nxt.decode("utf-8", errors="ignore").strip()
|
|
532
|
-
if line == "":
|
|
533
|
-
break
|
|
534
|
-
|
|
535
|
-
content_length = int(headers.get("content-length", "0") or 0)
|
|
536
|
-
if content_length <= 0:
|
|
537
|
-
# 라인 기반 폴백: 다음 라인을 JSON으로 시도
|
|
538
|
-
nxt = buf.readline()
|
|
539
|
-
if not nxt:
|
|
540
|
-
return None
|
|
541
|
-
text = nxt.decode("utf-8", errors="ignore").strip()
|
|
542
|
-
try:
|
|
543
|
-
return json.loads(text)
|
|
544
|
-
except Exception:
|
|
545
|
-
return None
|
|
546
|
-
|
|
547
|
-
body = buf.read(content_length)
|
|
548
|
-
try:
|
|
549
|
-
return json.loads(body.decode("utf-8"))
|
|
550
|
-
except Exception:
|
|
551
|
-
return None
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
def run():
|
|
555
|
-
"""MCP 서버 메인 루프.
|
|
556
|
-
|
|
557
|
-
JSON-RPC 2.0 프레이밍을 사용하여 VS Code/Copilot 등 MCP 클라이언트와 통신합니다.
|
|
558
|
-
"""
|
|
559
|
-
mcp = HossamMCP(name="hossam")
|
|
560
|
-
_register_all(mcp)
|
|
561
|
-
|
|
562
|
-
# DEV 모드 토글: 환경변수 `HOSSAM_MCP_DEV`가 "1"이면 DEBUG 레벨
|
|
563
|
-
dev_mode = os.getenv("HOSSAM_MCP_DEV", "0") == "1"
|
|
564
|
-
if dev_mode:
|
|
565
|
-
logging.getLogger().setLevel(logging.DEBUG)
|
|
566
|
-
logging.info("🛠 DEV 모드 활성화 (DEBUG 로그)")
|
|
567
|
-
|
|
568
|
-
logging.info(f"🚀 Hossam MCP 서버 시작 (도구 수: {len(mcp.list_tools())})")
|
|
569
|
-
|
|
570
|
-
try:
|
|
571
|
-
# JSON-RPC 2.0 메시지 처리 루프
|
|
572
|
-
while True:
|
|
573
|
-
# 메시지 수신
|
|
574
|
-
req = _read_json_rpc_message()
|
|
575
|
-
if req is None:
|
|
576
|
-
break
|
|
577
|
-
|
|
578
|
-
try:
|
|
579
|
-
request_id = req.get("id")
|
|
580
|
-
method = req.get("method")
|
|
581
|
-
params = req.get("params", {})
|
|
582
|
-
|
|
583
|
-
# 요청 로깅
|
|
584
|
-
_log_request(request_id, method, params)
|
|
585
|
-
|
|
586
|
-
# MCP 프로토콜 핸들링
|
|
587
|
-
if method == "initialize":
|
|
588
|
-
# 초기화 요청 처리
|
|
589
|
-
_send_response({
|
|
590
|
-
"jsonrpc": "2.0",
|
|
591
|
-
"id": request_id,
|
|
592
|
-
"result": {
|
|
593
|
-
"protocolVersion": PROTOCOL_VERSION,
|
|
594
|
-
"capabilities": {
|
|
595
|
-
"tools": {},
|
|
596
|
-
},
|
|
597
|
-
"serverInfo": {
|
|
598
|
-
"name": SERVER_NAME,
|
|
599
|
-
"version": SERVER_VERSION,
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
})
|
|
603
|
-
logging.info("✅ Initialize 응답 전송")
|
|
604
|
-
|
|
605
|
-
elif method == "notifications/initialized":
|
|
606
|
-
# 초기화 완료 알림 (응답 불필요)
|
|
607
|
-
logging.info("✅ Client initialized")
|
|
608
|
-
|
|
609
|
-
elif method == "ping":
|
|
610
|
-
# 핑 응답 (일부 클라이언트에서 사용)
|
|
611
|
-
_send_response({
|
|
612
|
-
"jsonrpc": "2.0",
|
|
613
|
-
"id": request_id,
|
|
614
|
-
"result": {}
|
|
615
|
-
})
|
|
616
|
-
|
|
617
|
-
elif method == "tools/list":
|
|
618
|
-
# 도구 목록 요청
|
|
619
|
-
tools_list = _build_tools_list(mcp)
|
|
620
|
-
_send_response({
|
|
621
|
-
"jsonrpc": "2.0",
|
|
622
|
-
"id": request_id,
|
|
623
|
-
"result": {
|
|
624
|
-
"tools": tools_list
|
|
625
|
-
}
|
|
626
|
-
})
|
|
627
|
-
|
|
628
|
-
elif method == "tools/call":
|
|
629
|
-
# 도구 호출
|
|
630
|
-
tool_name = params.get("name")
|
|
631
|
-
tool_args = params.get("arguments", {})
|
|
632
|
-
|
|
633
|
-
if not tool_name:
|
|
634
|
-
_send_error(request_id, -32602, "도구 이름 필요")
|
|
635
|
-
continue
|
|
636
|
-
|
|
637
|
-
# DataFrame 변환
|
|
638
|
-
mode = tool_args.get("mode") or tool_args.get("return") or "code"
|
|
639
|
-
if mode != "code" and "df" in tool_args:
|
|
640
|
-
tool_args["df"] = _df_from_any(tool_args["df"])
|
|
641
|
-
|
|
642
|
-
result = mcp.call(tool_name, **tool_args)
|
|
643
|
-
|
|
644
|
-
_send_response({
|
|
645
|
-
"jsonrpc": "2.0",
|
|
646
|
-
"id": request_id,
|
|
647
|
-
"result": {
|
|
648
|
-
"content": [
|
|
649
|
-
{
|
|
650
|
-
"type": "text",
|
|
651
|
-
"text": str(result)
|
|
652
|
-
}
|
|
653
|
-
]
|
|
654
|
-
}
|
|
655
|
-
})
|
|
656
|
-
|
|
657
|
-
else:
|
|
658
|
-
_send_error(request_id, -32601, f"Unknown method: {method}")
|
|
659
|
-
|
|
660
|
-
except json.JSONDecodeError as e:
|
|
661
|
-
logging.error(f"❌ Invalid JSON: {str(e)}")
|
|
662
|
-
_send_error(None, -32700, "Parse error")
|
|
663
|
-
except Exception as e:
|
|
664
|
-
logging.error(f"❌ Exception: {str(e)}")
|
|
665
|
-
_send_error(request_id, -32603, str(e))
|
|
666
|
-
|
|
667
|
-
except KeyboardInterrupt:
|
|
668
|
-
logging.info("👋 서버 종료 (KeyboardInterrupt)")
|
|
669
|
-
except Exception as e:
|
|
670
|
-
logging.error(f"❌ 서버 오류: {str(e)}")
|
|
671
|
-
sys.exit(1)
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
if __name__ == "__main__":
|
|
675
|
-
run()
|
hossam-0.3.19.dist-info/RECORD
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
hossam/NotoSansKR-Regular.ttf,sha256=0SCufUQwcVWrWTu75j4Lt_V2bgBJIBXl1p8iAJJYkVY,6185516
|
|
2
|
-
hossam/__init__.py,sha256=OHMVqGLMSbqM06zdwND9FITToVDG_fhzMYxreVzWEG8,2801
|
|
3
|
-
hossam/data_loader.py,sha256=UpC_gn-xUWij0s-MO51qrzJNz3b5-RCz1N6esQMZUJM,6320
|
|
4
|
-
hossam/hs_classroom.py,sha256=WfiGy2VHlfnqsslV4zmbJqjhvf2SuqCkcumpN81OCkE,26159
|
|
5
|
-
hossam/hs_gis.py,sha256=9ER8gXG2Or0DZ1fpbJR84WsNVPcxu788FsNtR6LsEgo,11379
|
|
6
|
-
hossam/hs_plot.py,sha256=7cngzrEeVeBvANyReI9kd3yHZGMOFZjvgBbBGA7rT2E,78467
|
|
7
|
-
hossam/hs_prep.py,sha256=RMeM6QPR-dsk3X_crJity8Tva8xY-mOXlcG3L7W-qjg,36220
|
|
8
|
-
hossam/hs_stats.py,sha256=2P_CeitniiiOcuX1El6si7IUOayThxIVBbuCaCASNVY,117746
|
|
9
|
-
hossam/hs_timeserise.py,sha256=loRofR-m2NMxHaDEWDhZjo6DwayEf4c7qkSoCErfBWY,42165
|
|
10
|
-
hossam/hs_util.py,sha256=E4LnzPlRdWeqICv7TtTL9DT5PogqBhOuTgYiaav565U,7461
|
|
11
|
-
hossam/leekh.png,sha256=1PB5NQ24SDoHA5KMiBBsWpSa3iniFcwFTuGwuOsTHfI,6395
|
|
12
|
-
hossam/mcp/__init__.py,sha256=uOFs4lTDpYoB_YZM4wOENkB0yWD0m8xuY7p4RQaskyE,338
|
|
13
|
-
hossam/mcp/hs_classroom.py,sha256=u_5WUvNSS9e9wmBAxsMeCty5lnposZC1w-pud7jMhuk,759
|
|
14
|
-
hossam/mcp/hs_gis.py,sha256=PyfzySp_degVbg7O3h0BlDfZJjgclCQKScku-klcuX8,1176
|
|
15
|
-
hossam/mcp/hs_plot.py,sha256=3e9FabRrf0FEwUTtld3e3irJFWzmU7EuV99GMfzLqnY,2474
|
|
16
|
-
hossam/mcp/hs_prep.py,sha256=QcaUb-kggexi-chMCRoN104YXKsDHJNZJifDJkadef4,2624
|
|
17
|
-
hossam/mcp/hs_stats.py,sha256=9ODc3rXxzOasng1nvMDctbPC_T6gyeN72-hOiDCJ2eQ,796
|
|
18
|
-
hossam/mcp/hs_timeserise.py,sha256=Uc5ZKLv_sBe1tUdSZ0pmBuAFOQ_yX2MH8Me3DVeBp7c,762
|
|
19
|
-
hossam/mcp/hs_util.py,sha256=XsjOb9K5PUeAStgne-BluVUGyt0QEzN5wmFX77-yYGk,1237
|
|
20
|
-
hossam/mcp/loader.py,sha256=Ib11QUG8gV1V0TkMUz1g5kGsFJCApiwiwQ9N1cCy7bA,857
|
|
21
|
-
hossam/mcp/server.py,sha256=4B6ri8xwX22sA6OlNOWC4wN7Uwfgtt4gQ_HvROMNINo,21270
|
|
22
|
-
hossam-0.3.19.dist-info/licenses/LICENSE,sha256=nIqzhlcFY_2D6QtFsYjwU7BWkafo-rUJOQpDZ-DsauI,941
|
|
23
|
-
hossam-0.3.19.dist-info/METADATA,sha256=AxoQn7umetTYtF_Lz1CrxsDIcesCl-7TW75b7knA55g,5678
|
|
24
|
-
hossam-0.3.19.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
25
|
-
hossam-0.3.19.dist-info/entry_points.txt,sha256=0y7c7g_swXFS6See69i-4q_shcwUs-wIalfuvuYiA9I,53
|
|
26
|
-
hossam-0.3.19.dist-info/top_level.txt,sha256=_-7bwjhthHplWhywEaHIJX2yL11CQCaLjCNSBlk6wiQ,7
|
|
27
|
-
hossam-0.3.19.dist-info/RECORD,,
|
|
File without changes
|