Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion assemblymcp/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

class Settings(BaseSettings):
# Core Settings
assembly_api_key: str | None = Field(None, description="National Assembly API Key")
api_key: str | None = Field(None, description="National Assembly API Key")
default_assembly_age: str = Field("22", description="Default Assembly Age (e.g. 22)")

# Logging Settings
Expand Down
6 changes: 3 additions & 3 deletions assemblymcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@

# Initialize API Client globally to load specs once
try:
client = AssemblyAPIClient(api_key=settings.assembly_api_key)
client = AssemblyAPIClient(api_key=settings.api_key)
except Exception as e:
logger.error(f"Failed to initialize client: {e}")
client = None
Expand Down Expand Up @@ -106,7 +106,7 @@ async def get_assembly_info() -> str:
return "Error: API Client not initialized. Please check API key configuration."

try:
api_key_status = "configured" if settings.assembly_api_key else "not configured"
api_key_status = "configured" if settings.api_key else "not configured"
service_count = len(client.service_map)
return (
"AssemblyMCP – 대한민국 국회 OpenAPI (Korean National Assembly Open API)\n"
Expand Down Expand Up @@ -704,7 +704,7 @@ def main():
"""Run the MCP server"""
sys.stdout.reconfigure(line_buffering=True)
# Validate settings on startup (but don't fail if API key is missing yet)
if not settings.assembly_api_key:
if not settings.api_key:
logger.warning("ASSEMBLY_API_KEY is not configured. The server will run but tools will fail.")

# Check for transport configuration
Expand Down
116 changes: 37 additions & 79 deletions assemblymcp/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def normalize_age(val: Any) -> str:
retry=retry_if_exception_type((AssemblyAPIError, httpx.RequestError)),
reraise=True,
)
async def _get_data_with_retry(client: AssemblyAPIClient, service_id: str, params: dict[str, Any]) -> dict[str, Any]:
async def _get_data_with_retry(client: AssemblyAPIClient, service_id: str, params: dict[str, Any]) -> list[Any] | str:
"""Fetch data from API with retry logic."""
try:
return await client.get_data(service_id_or_name=service_id, params=params)
Expand All @@ -64,23 +64,14 @@ async def _get_data_with_retry(client: AssemblyAPIClient, service_id: str, param


def _collect_rows(raw_data: Any) -> list[dict[str, Any]]:
"""Walk nested API response objects and return every dict row."""
rows: list[dict[str, Any]] = []
"""Convert get_data() result to a flat list of dicts.

def _walk(node: Any) -> None:
if isinstance(node, dict):
if "row" in node and isinstance(node["row"], list):
for entry in node["row"]:
if isinstance(entry, dict):
rows.append(entry)
for value in node.values():
_walk(value)
elif isinstance(node, list):
for item in node:
_walk(item)

_walk(raw_data)
return rows
get_data() returns list[BaseModel] (generated types) or list[dict] (fallback).
Both cases are handled uniformly here.
"""
if not raw_data or isinstance(raw_data, str):
return []
return [item.model_dump() if hasattr(item, "model_dump") else item for item in raw_data]


class DiscoveryService:
Expand Down Expand Up @@ -150,15 +141,17 @@ async def list_services(self, keyword: str = "") -> list[dict[str, str]]:
results.sort(key=lambda x: x["name"])
return results

async def call_raw(self, service_id_or_name: str, params: dict[str, Any]) -> dict[str, Any]:
async def call_raw(self, service_id_or_name: str, params: dict[str, Any]) -> list[dict[str, Any]]:
"""
Call a specific API service with raw parameters.
Returns rows as a list of dicts (JSON-serializable).
"""
# 파라미터 자동 보정 적용
normalized_params = self._normalize_params(params)

try:
return await _get_data_with_retry(self.client, service_id_or_name, normalized_params)
raw = await _get_data_with_retry(self.client, service_id_or_name, normalized_params)
return _collect_rows(raw)
except AssemblyAPIError as e:
error_msg = str(e)

Expand Down Expand Up @@ -919,68 +912,33 @@ async def get_committee_members(
params["COMMITTEE_NAME"] = committee_name

raw_data = await _get_data_with_retry(self.client, self.COMMITTEE_MEMBER_LIST_ID, params)

# Explicitly check for INFO-200 (No corresponding data) from the raw API response
# Structure is usually { "service_name": [ { "head": [...] }, { "row": [...] } ] }
# or just { "RESULT": { "CODE": "...", "MESSAGE": "..." } }
service_key = list(raw_data.keys())[0] if raw_data else None
if service_key and isinstance(raw_data[service_key], list) and len(raw_data[service_key]) > 0:
head_section = raw_data[service_key][0].get("head")
if head_section and len(head_section) > 1:
result = head_section[1].get("RESULT")
if result and result.get("CODE") == "INFO-200":
msg = result.get("MESSAGE")

error_details = {
"error_type": "DATA_NOT_FOUND",
"api_code": result.get("CODE"),
"api_message": msg,
"query_info": {
"committee_name": committee_name,
"committee_code": committee_code,
},
"suggestion": (
"이 위원회에 대한 위원 명단 데이터가 국회 OpenAPI에 없거나, "
"검색 조건(이름)이 정확하지 않을 수 있습니다. "
"get_committee_list()를 호출하여 정확한 committee_code를 "
"확인 후 다시 시도하거나, 다른 검색어를 사용해보세요."
),
}

# If searched by name and no code, try to find suggestions
if committee_name and not committee_code:
try:
candidates = await self.get_committee_list(committee_name=committee_name)
valid_candidates = []
for c in candidates:
if c.HR_DEPT_CD and c.HR_DEPT_CD != "None":
valid_candidates.append(f"{c.COMMITTEE_NAME}(코드: {c.HR_DEPT_CD})")

if valid_candidates:
error_details["suggestion"] = (
f"입력하신 위원회명 '{committee_name}'에 대해 "
"위원 명단을 찾을 수 없습니다. "
f"다음과 같은 관련 위원회가 있습니다. "
f"해당 코드(committee_code)로 재시도해보세요: "
f"{', '.join(valid_candidates)}"
)
else:
error_details["suggestion"] = (
f"'{committee_name}'(으)로 검색된 위원회 중 "
f"유효한 코드(HR_DEPT_CD)를 가진 위원회가 없습니다. "
"이는 해당 위원회 명단이 OpenAPI에 없거나, "
"이름이 정확하지 않을 수 있습니다. "
"get_committee_list()를 호출하여 "
"전체 위원회 목록을 확인해보세요."
)
except Exception as e:
logger.warning(f"Error generating suggestions for '{committee_name}': {e}")
# Fallback to generic suggestion if suggestion generation fails

return {"error": error_details}

rows = _collect_rows(raw_data)

if not rows and committee_name and not committee_code:
# 빈 결과일 때 관련 위원회 코드 제안
suggestion = (
"이 위원회에 대한 위원 명단 데이터가 국회 OpenAPI에 없거나, "
"검색 조건(이름)이 정확하지 않을 수 있습니다. "
"get_committee_list()를 호출하여 정확한 committee_code를 "
"확인 후 다시 시도하거나, 다른 검색어를 사용해보세요."
)
try:
candidates = await self.get_committee_list(committee_name=committee_name)
valid_candidates = [
f"{c.COMMITTEE_NAME}(코드: {c.HR_DEPT_CD})"
for c in candidates
if c.HR_DEPT_CD and c.HR_DEPT_CD != "None"
]
if valid_candidates:
suggestion = (
f"입력하신 위원회명 '{committee_name}'에 대해 위원 명단을 찾을 수 없습니다. "
f"다음과 같은 관련 위원회가 있습니다. "
f"해당 코드(committee_code)로 재시도해보세요: {', '.join(valid_candidates)}"
)
except Exception as e:
logger.warning(f"Error generating suggestions for '{committee_name}': {e}")
return {"error": {"error_type": "DATA_NOT_FOUND", "suggestion": suggestion}}

# CRITICAL FIX: The API sometimes ignores HR_DEPT_CD and returns all members.
# We must manually filter by committee_code if it was provided.
if committee_code:
Expand Down
48 changes: 48 additions & 0 deletions docs/PROJECT_STATUS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# AssemblyMCP Project Status

이 문서는 현재 워크트리 상태, 최근 변경 방향, 다음 작업 우선순위를 빠르게 파악하기 위한 운영 메모입니다.

## Current Status

- 기준 시점: 2026-04-01
- 개발 상태: 실행 가능, 핵심 테스트 통과
- 현재 작업 브랜치: `refactor/pydantic-model-first`
- 핵심 연동 상태: `assembly-api-client` `v1.2.6` 계약 반영 완료

## Verified State

- `AssemblyMCP`: `uv run pytest -q` -> `67 passed`
- `assembly-api-client`: `uv run pytest -q` -> `254 passed`
- 실데이터 스모크 확인 완료:
- 최근 법안 조회
- 의원 정보 조회
- 위원회 일정 조회
- 위원회 목록 조회

## Recent Technical Changes

- `assembly-api-client>=1.2.6`로 의존성 상향
- `get_data()`의 반환 계약 변경 반영
- 과거: 중첩된 raw dict 중심 응답
- 현재: `list[BaseModel]` 또는 `list[dict]`
- `assemblymcp/services.py`에서 `_collect_rows()`로 응답 평탄화 통일
- 관련 테스트 전반을 새 계약 기준으로 정리

## Operational Notes

- `.env`의 `ASSEMBLY_API_KEY` 설정 시 실 API 호출 가능
- `assemblymcp/config.py` 설정 필드는 `assembly_api_key`가 아니라 `api_key`
- `MeetingService.search_meetings()`는 `page_size`가 아니라 `limit` 인자를 사용

## Remaining Workspace Notes

- 이 저장소에는 코드 미완료 흔적이 거의 없음
- 별도 저장소 `assembly-api-client`에는 `uv.lock` 수정이 남아 있음
- 이 저장소의 `.agent/` 메모는 이 문서로 통합함

## Suggested Next Steps

1. `assembly-api-client`의 `uv.lock` 변경을 유지할지 결정
2. 필요하면 Docker 빌드/런타임 검증 추가
3. 배포 전 실제 사용 시나리오 기준 스모크 스크립트 정리
4. 운영 문서가 더 필요하면 `docs/HANDOVER_GUIDE.md`와 역할 분리 유지
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ license = "MIT"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"assembly-api-client>=1.1.0",
"assembly-api-client>=1.2.6",
"beautifulsoup4>=4.14.2",
"fastmcp>=2.13.1",
"httpx>=0.28.1",
Expand Down
Loading