#!/usr/bin/env python3
"""
Guardian News MCP Server using FastMCP
"""
import asyncio
import sys
from fastmcp import FastMCP
from pydantic import BaseModel, Field
# --- ๐ ์์ ๋ ๋ถ๋ถ: get_available_sections๋ฅผ import์ ์ถ๊ฐ ---
from .tools import search_news, get_available_sections, get_full_article_text, get_news_trend, get_related_topics
from typing import Optional
import os
from dotenv import load_dotenv
# .env ํ์ผ ๋ก๋
load_dotenv()
# FastMCP ๊ฐ์ฒด ์์ฑ
mcp = FastMCP()
class RelatedTopicsRequest(BaseModel):
query: str = Field(..., description="์ฐ๊ด ํ ํฝ์ ๋ถ์ํ ๋ด์ค ํค์๋")
page_size: int = Field(20, description="๋ถ์ํ ์ต์ ๊ธฐ์ฌ ์ (๊ธฐ๋ณธ๊ฐ: 20, ์ต๋: 50)", ge=1, le=50)
class NewsTrendRequest(BaseModel):
query: str = Field(..., description="๊ฒ์ํ ๋ด์ค ํค์๋ (์: '์ธ๊ณต์ง๋ฅ', '๊ธฐ์ ', '์ ์น')")
start_date: str = Field(..., description="๊ฒ์ ์์์ผ (YYYY-MM-DD ํ์, ์: '2023-10-01')")
end_date: str = Field(..., description="๊ฒ์ ์ข
๋ฃ์ผ (YYYY-MM-DD ํ์, ์: '2023-10-27')")
class ArticleRequest(BaseModel):
url: str = Field(..., description="๋ณธ๋ฌธ์ ๊ฐ์ ธ์ฌ ๊ธฐ์ฌ์ URL")
class SearchRequest(BaseModel):
query: str = Field(..., description="๊ฒ์ํ ๋ด์ค ํค์๋ (์: '์ธ๊ณต์ง๋ฅ', '๊ธฐ์ ', '์ ์น')")
page_size: int = Field(5, description="๊ฐ์ ธ์ฌ ๊ธฐ์ฌ์ ์ (๊ธฐ๋ณธ๊ฐ: 5, ์ต๋: 50)", ge=1, le=50)
section: Optional[str] = Field(None, description="๊ฒ์ํ ๋ด์ค ์น์
(์: 'technology')")
from_date: Optional[str] = Field(None, description="๊ฒ์ ์์์ผ (YYYY-MM-DD ํ์, ์: '2023-10-01')")
to_date: Optional[str] = Field(None, description="๊ฒ์ ์ข
๋ฃ์ผ (YYYY-MM-DD ํ์, ์: '2023-10-27')")
# ์ค์ ํจ์๋ค (MCP ๋๊ตฌ์ ๋ถ๋ฆฌ)
async def get_related_topics_impl(req: RelatedTopicsRequest):
"""์ค์ ์ฐ๊ด ํ ํฝ ๋ถ์ ๊ตฌํ"""
try:
return await asyncio.to_thread(get_related_topics, req.query, req.page_size)
except Exception as e:
return {"error": f"์ฐ๊ด ํ ํฝ ๋ถ์ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค: {str(e)}"}
async def get_news_trend_impl(req: NewsTrendRequest):
"""์ค์ ๋ด์ค ํธ๋ ๋ ์กฐํ ๊ตฌํ"""
try:
result = get_news_trend(req.query, req.start_date, req.end_date)
return result
except Exception as e:
return {"error": f"๋ด์ค ํธ๋ ๋ ์กฐํ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค: {str(e)}"}
async def search_news_impl(req: SearchRequest):
"""์ค์ ๋ด์ค ๊ฒ์ ๊ตฌํ"""
try:
result = search_news(
req.query,
req.page_size,
req.section,
req.from_date,
req.to_date
)
return result
except Exception as e:
return {"error": f"๋ด์ค ๊ฒ์ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค: {str(e)}"}
async def get_available_sections_impl():
"""์ค์ ์น์
๋ชฉ๋ก ์กฐํ ๊ตฌํ"""
try:
return get_available_sections()
except Exception as e:
return {"error": f"์น์
๋ชฉ๋ก ์กฐํ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค: {str(e)}"}
async def health_impl():
"""์ค์ ์๋น์ค ์ํ ํ์ธ ๊ตฌํ"""
api_key = os.environ.get("GUARDIAN_API_KEY", "")
api_key_status = "์ค์ ๋จ" if api_key else "์ค์ ๋์ง ์์"
return {
"status": "ok",
"environment": {
"guardian_api_key": api_key_status,
"api_key_preview": api_key[:10] + "..." if api_key else "None"
}
}
async def get_full_article_text_impl(req: ArticleRequest):
"""์ค์ ๊ธฐ์ฌ ๋ณธ๋ฌธ ์กฐํ ๊ตฌํ"""
try:
# get_full_article_text๋ ๋๊ธฐ ํจ์์ด๋ฏ๋ก, to_thread๋ก ์คํํ์ฌ ๋น๋๊ธฐ ๋ฃจํ๋ฅผ ๋ง์ง ์๋๋ก ํฉ๋๋ค.
return await asyncio.to_thread(get_full_article_text, req.url)
except Exception as e:
return {"error": f"๊ธฐ์ฌ ๋ณธ๋ฌธ ์กฐํ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค: {str(e)}"}
async def get_tool_definitions_impl():
"""์ค์ ๋๊ตฌ ์ ์ ๊ตฌํ"""
tools = [
{
"name": "health",
"description": "์๋น์ค ์ํ ํ์ธ",
"parameters": {"type": "object", "properties": {}, "required": []}
},
{
"name": "get_sections_tool",
"description": "The Guardian API์์ ์ ๊ณตํ๋ ๋ชจ๋ ๋ด์ค ์น์
๋ชฉ๋ก์ ๊ฐ์ ธ์ต๋๋ค.",
"parameters": {"type": "object", "properties": {}, "required": []}
},
{
"name": "search_news_tool",
"description": "The Guardian API๋ฅผ ์ฌ์ฉํ์ฌ ํน์ ํค์๋์ ๋ํ ์ต์ ๋ด์ค๋ฅผ ๊ฒ์ํฉ๋๋ค. ์น์
๋ฐ ๋ ์ง ํํฐ๋ง์ ์ง์ํฉ๋๋ค. ๊ฒ์์ด๋ ๋ฐ๋์ ์์ด(English)์ฌ์ผ ํฉ๋๋ค.",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "๊ฒ์ํ ๋ด์ค ํค์๋ (์: 'AI', 'technology', 'politics')"},
"page_size": {"type": "integer", "description": "๊ฐ์ ธ์ฌ ๊ธฐ์ฌ์ ์ (๊ธฐ๋ณธ๊ฐ: 5, ์ต๋: 50)", "default": 5, "minimum": 1, "maximum": 50},
"section": {"type": "string", "description": "๊ฒ์์ ์ ํํ ๋ด์ค ์น์
(์: 'technology', 'world')"},
"from_date": {"type": "string", "description": "๊ฒ์ ์์์ผ (YYYY-MM-DD ํ์)"},
"to_date": {"type": "string", "description": "๊ฒ์ ์ข
๋ฃ์ผ (YYYY-MM-DD ํ์)"}
},
"required": ["query"]
}
},
{
"name": "get_full_article_text_tool",
"description": "๊ธฐ์ฌ URL์ ์
๋ ฅ๋ฐ์ ํด๋น ๊ธฐ์ฌ์ ์ ์ฒด ๋ณธ๋ฌธ ํ
์คํธ๋ฅผ ๊ฐ์ ธ์ต๋๋ค.",
"parameters": {
"type": "object",
"properties": {
"url": {
"type": "string",
"description": "๋ณธ๋ฌธ์ ๊ฐ์ ธ์ฌ ๊ธฐ์ฌ์ ์ ์ฒด URL"
}
},
"required": ["url"]
}
},
{
"name": "get_news_trend_tool",
"description": "์ฃผ์ด์ง ๊ธฐ๊ฐ ๋์ ํน์ ํค์๋์ ๋ํ ์๋ณ ๋ด์ค ๊ธฐ์ฌ ์๋ฅผ ์ง๊ณํ์ฌ ํธ๋ ๋ ๋ฐ์ดํฐ๋ฅผ ๋ฐํํฉ๋๋ค.",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "๊ฒ์ํ ๋ด์ค ํค์๋ (์: '์ธ๊ณต์ง๋ฅ', '๊ธฐ์ ', '์ ์น')"},
"start_date": {"type": "string", "description": "๊ฒ์ ์์์ผ (YYYY-MM-DD ํ์, ์: '2023-10-01')"},
"end_date": {"type": "string", "description": "๊ฒ์ ์ข
๋ฃ์ผ (YYYY-MM-DD ํ์, ์: '2023-10-27')"}
},
"required": ["query", "start_date", "end_date"]
}
},
{
"name": "get_related_topics_tool",
"description": "ํน์ ํค์๋์ ๊ด๋ จ๋ ๊ธฐ์ฌ๋ค์ ํ๊ทธ๋ฅผ ๋ถ์ํ์ฌ ๊ฐ์ฅ ๋น๋๊ฐ ๋์ ์ฐ๊ด ํ ํฝ์ ๋ฐํํฉ๋๋ค.",
"parameters": {
"type": "object",
"properties": {"query": {"type": "string", "description": "์ฐ๊ด ํ ํฝ์ ๋ถ์ํ ๋ด์ค ํค์๋"}, "page_size": {"type": "integer", "description": "๋ถ์ํ ์ต์ ๊ธฐ์ฌ ์ (๊ธฐ๋ณธ๊ฐ: 20, ์ต๋: 50)", "default": 20, "minimum": 1, "maximum": 50}},
"required": ["query"]
}
}
]
return {"tools": tools}
@mcp.tool()
async def health():
"""์๋น์ค ์ํ ํ์ธ"""
return await health_impl()
@mcp.tool()
async def get_related_topics_tool(query: str, page_size: int = 20):
"""ํน์ ํค์๋์ ๊ด๋ จ๋ ๋ด์ค ํ ํฝ์ ๋ถ์ํฉ๋๋ค."""
req = RelatedTopicsRequest(query=query, page_size=page_size)
return await get_related_topics_impl(req)
@mcp.tool()
async def get_sections_tool():
"""The Guardian์์ ๊ฒ์ ๊ฐ๋ฅํ ๋ชจ๋ ๋ด์ค ์น์
๋ชฉ๋ก์ ์ ๊ณตํฉ๋๋ค."""
return await get_available_sections_impl()
@mcp.tool()
async def search_news_tool(
query: str,
page_size: int = 5,
section: Optional[str] = None,
from_date: Optional[str] = None,
to_date: Optional[str] = None
):
"""The Guardian API๋ฅผ ์ฌ์ฉํ์ฌ ํน์ ํค์๋์ ๋ํ ์ต์ ๋ด์ค๋ฅผ ๊ฒ์ํฉ๋๋ค."""
req = SearchRequest(
query=query,
page_size=page_size,
section=section,
from_date=from_date,
to_date=to_date
)
return await search_news_impl(req)
@mcp.tool()
async def get_tool_definitions():
"""MCP ๋๊ตฌ ์ ์๋ฅผ JSON ํ์์ผ๋ก ์ ๊ณตํฉ๋๋ค."""
return await get_tool_definitions_impl()
@mcp.tool()
async def get_full_article_text_tool(url: str):
"""๊ธฐ์ฌ URL์ ์
๋ ฅ๋ฐ์ ํด๋น ๊ธฐ์ฌ์ ์ ์ฒด ๋ณธ๋ฌธ ํ
์คํธ๋ฅผ ๊ฐ์ ธ์ต๋๋ค."""
req = ArticleRequest(url=url)
return await get_full_article_text_impl(req)
@mcp.tool()
async def get_news_trend_tool(query: str, start_date: str, end_date: str):
"""์ฃผ์ด์ง ๊ธฐ๊ฐ ๋์ ํน์ ํค์๋์ ๋ํ ์๋ณ ๋ด์ค ๊ธฐ์ฌ ์๋ฅผ ์ง๊ณํ์ฌ ํธ๋ ๋ ๋ฐ์ดํฐ๋ฅผ ๋ฐํํฉ๋๋ค."""
req = NewsTrendRequest(query=query, start_date=start_date, end_date=end_date)
return await get_news_trend_impl(req)
async def main():
"""MCP ์๋ฒ๋ฅผ ์คํํฉ๋๋ค."""
print("MCP Guardian News Server starting...", file=sys.stderr)
print("Server: guardian-news-service", file=sys.stderr)
print("Available tools: health,get_full_article_text_tool, search_news_tool, get_sections_tool, get_tool_definitions, get_news_trend_tool, get_related_topics_tool", file=sys.stderr)
try:
await mcp.run_stdio_async()
except Exception as e:
print(f"Server error: {e}", file=sys.stderr)
import traceback
traceback.print_exc(file=sys.stderr)
raise
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
print("Server stopped by user", file=sys.stderr)
except Exception as e:
print(f"Server failed: {e}", file=sys.stderr)
sys.exit(1)