epl-mcp-server
Version:
EPL 축구 소식을 위한 MCP Server
547 lines (453 loc) • 12.7 kB
Markdown
# EPL MCP Server 웹 API 사용 가이드
## 설치 및 실행
### 1. 프로젝트 설정
```bash
# 웹 API 전용 디렉토리 생성
mkdir epl-mcp-web-api
cd epl-mcp-web-api
# 파일들 생성 후
npm install
# 환경변수 설정 (.env)
SUPABASE_URL=your-supabase-url
SUPABASE_ANON_KEY=your-anon-key
PORT=3001
```
### 2. 개발 모드 실행
```bash
npm run dev
```
### 3. 프로덕션 빌드 및 실행
```bash
npm run serve
```
## API 엔드포인트 사용법
### 기본 URL
```
http://localhost:3001/api
```
### 1. 핫 이슈 조회
```bash
# GET /api/hot-issues
curl "http://localhost:3001/api/hot-issues?timeframe=today&limit=5"
# JavaScript에서 사용
const response = await fetch('http://localhost:3001/api/hot-issues?timeframe=week&limit=10');
const data = await response.json();
console.log(data.data.hot_issues);
```
### 2. 선수 뉴스 조회
```bash
# GET /api/player-news
curl "http://localhost:3001/api/player-news?player_name=손흥민&timeframe=7days"
# JavaScript에서 사용
const playerNews = await fetch('http://localhost:3001/api/player-news?player_name=Son Heung-min&timeframe=1month')
.then(res => res.json());
```
### 3. 이적 뉴스 조회
```bash
# GET /api/transfer-news
curl "http://localhost:3001/api/transfer-news?timeframe=1month&player_name=Haaland"
```
### 4. 경기 이슈 조회
```bash
# GET /api/match-issues
curl "http://localhost:3001/api/match-issues?date_range=2024-06-20:2024-06-28&teams=Arsenal,Chelsea"
```
### 5. 트윗 검색
```bash
# POST /api/search-tweets
curl -X POST "http://localhost:3001/api/search-tweets" \
-H "Content-Type: application/json" \
-d '{
"keywords": ["goal", "victory", "match"],
"timeframe": "7days",
"limit": 15
}'
# JavaScript에서 사용
const searchResults = await fetch('http://localhost:3001/api/search-tweets', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
keywords: ['transfer', 'signing'],
timeframe: '1month',
limit: 20
})
}).then(res => res.json());
```
### 6. 카테고리별 트렌드
```bash
# GET /api/category-trends/:category
curl "http://localhost:3001/api/category-trends/player?timeframe=1week"
curl "http://localhost:3001/api/category-trends/transfer?timeframe=1month"
```
### 7. 해시태그 추천
```bash
# POST /api/suggest-hashtags
curl -X POST "http://localhost:3001/api/suggest-hashtags" \
-H "Content-Type: application/json" \
-d '{
"content": "Arsenal signed a new striker from Brighton",
"category": "transfer"
}'
```
### 8. 포스트 템플릿
```bash
# GET /api/post-templates/:type
curl "http://localhost:3001/api/post-templates/transfer_news"
curl "http://localhost:3001/api/post-templates/match_preview"
```
## 프론트엔드 통합 예시
### React 컴포넌트 예시
```jsx
import React, { useState, useEffect } from 'react';
const EPLHotIssues = () => {
const [hotIssues, setHotIssues] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchHotIssues = async () => {
try {
const response = await fetch('http://localhost:3001/api/hot-issues?timeframe=today&limit=10');
const data = await response.json();
if (data.success) {
setHotIssues(data.data.hot_issues);
}
} catch (error) {
console.error('Failed to fetch hot issues:', error);
} finally {
setLoading(false);
}
};
fetchHotIssues();
}, []);
if (loading) return <div>Loading...</div>;
return (
<div>
<h2>오늘의 EPL 핫 이슈</h2>
{hotIssues.map((issue, index) => (
<div key={issue.id || index} className="issue-card">
<p>{issue.content}</p>
<small>by {issue.author} • {issue.engagement} engagements</small>
<div>
{issue.hashtags.map(tag => (
<span key={tag} className="hashtag">#{tag}</span>
))}
</div>
</div>
))}
</div>
);
};
export default EPLHotIssues;
```
### Vue.js 컴포넌트 예시
```vue
<template>
<div>
<h2>선수 뉴스 검색</h2>
<input v-model="playerName" placeholder="선수 이름 입력" />
<button @click="searchPlayerNews">검색</button>
<div v-if="playerNews.length > 0">
<div v-for="news in playerNews" :key="news.id" class="news-item">
<p>{{ news.content }}</p>
<small>{{ news.created_at }} • {{ news.engagement }} engagements</small>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
playerName: '',
playerNews: []
}
},
methods: {
async searchPlayerNews() {
if (!this.playerName) return;
try {
const response = await fetch(
`http://localhost:3001/api/player-news?player_name=${encodeURIComponent(this.playerName)}&timeframe=1month`
);
const data = await response.json();
if (data.success) {
this.playerNews = data.data.player_news;
}
} catch (error) {
console.error('Failed to fetch player news:', error);
}
}
}
}
</script>
```
## 배포 옵션
### 1. Vercel (권장)
```bash
# vercel.json 생성
{
"version": 2,
"builds": [
{
"src": "dist/web-api-server.js",
"use": "@vercel/node"
}
],
"routes": [
{
"src": "/(.*)",
"dest": "dist/web-api-server.js"
}
]
}
```
### 2. Railway
```bash
# railway up
```
### 3. Render
```bash
# render.yaml 생성
services:
- type: web
name: epl-mcp-api
env: node
buildCommand: npm run build
startCommand: npm start
```
## 보안 고려사항
### 1. CORS 설정
```typescript
app.use(cors({
origin: ['http://localhost:3000', 'https://yourdomain.com'],
credentials: true
}));
```
### 2. Rate Limiting
```typescript
import rateLimit from 'express-rate-limit';
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15분
max: 100, // 최대 100요청
message: 'Too many requests from this IP'
});
app.use('/api/', limiter);
```
### 3. API 키 인증 (선택사항)
```typescript
const authenticateApiKey = (req: any, res: any, next: any) => {
const apiKey = req.headers['x-api-key'];
if (!apiKey || apiKey !== process.env.API_KEY) {
return res.status(401).json({ success: false, error: 'Invalid API key' });
}
next();
};
// 보호된 엔드포인트에 적용
app.use('/api/admin/', authenticateApiKey);
```
## 모니터링 및 로깅
### 1. 요청 로깅
```typescript
import morgan from 'morgan';
app.use(morgan('combined'));
```
### 2. 에러 추적
```typescript
import * as Sentry from '@sentry/node';
Sentry.init({ dsn: process.env.SENTRY_DSN });
app.use(Sentry.Handlers.errorHandler());
```
## 성능 최적화
### 1. 캐싱 구현
```typescript
import NodeCache from 'node-cache';
const cache = new NodeCache({ stdTTL: 600 }); // 10분 캐시
app.get('/api/hot-issues', async (req, res) => {
const cacheKey = `hot-issues-${req.query.timeframe}-${req.query.limit}`;
const cached = cache.get(cacheKey);
if (cached) {
return res.json({ success: true, data: cached, cached: true });
}
const result = await eplService.getHotIssues(/* ... */);
cache.set(cacheKey, result);
res.json({ success: true, data: result });
});
```
### 2. 데이터베이스 연결 풀링
```typescript
// Supabase는 자동으로 연결 풀링을 관리하지만,
// 필요시 커스텀 설정 가능
const supabase = createClient(url, key, {
db: {
schema: 'public'
},
auth: {
autoRefreshToken: false,
persistSession: false
}
});
```
## 웹훅 지원 (실시간 업데이트)
### 1. Supabase 실시간 구독
```typescript
// 실시간 트윗 업데이트 웹훅
app.post('/api/webhook/tweet-update', async (req, res) => {
const { record, eventType } = req.body;
// 클라이언트들에게 실시간 업데이트 전송
io.emit('tweet-update', {
type: eventType,
data: record
});
res.json({ success: true });
});
```
### 2. WebSocket 지원
```typescript
import { Server } from 'socket.io';
import http from 'http';
const server = http.createServer(app);
const io = new Server(server, {
cors: {
origin: "*",
methods: ["GET", "POST"]
}
});
io.on('connection', (socket) => {
console.log('Client connected:', socket.id);
socket.on('subscribe-hot-issues', () => {
socket.join('hot-issues');
});
socket.on('disconnect', () => {
console.log('Client disconnected:', socket.id);
});
});
```
## 배치 작업 API
### 1. 대량 데이터 처리
```typescript
app.post('/api/batch/analyze-trends', async (req, res) => {
try {
const { categories, timeframe } = req.body;
const results = [];
for (const category of categories) {
const trends = await eplService.getCategoryTrends(category, timeframe);
results.push(trends);
}
res.json({ success: true, data: results });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
```
### 2. 스케줄된 작업
```typescript
import cron from 'node-cron';
// 매시간 캐시 클리어
cron.schedule('0 * * * *', () => {
cache.flushAll();
console.log('Cache cleared');
});
// 매일 트렌드 분석 리포트 생성
cron.schedule('0 9 * * *', async () => {
try {
const dailyReport = await generateDailyReport();
console.log('Daily report generated:', dailyReport);
} catch (error) {
console.error('Failed to generate daily report:', error);
}
});
```
## 프론트엔드 라이브러리/SDK
### 1. JavaScript SDK 생성
```typescript
// epl-mcp-sdk.js
class EPLMCPClient {
constructor(baseURL = 'http://localhost:3001/api', apiKey = null) {
this.baseURL = baseURL;
this.apiKey = apiKey;
}
async request(endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`;
const headers = {
'Content-Type': 'application/json',
...(this.apiKey && { 'X-API-Key': this.apiKey }),
...options.headers
};
const response = await fetch(url, { ...options, headers });
const data = await response.json();
if (!data.success) {
throw new Error(data.error);
}
return data.data;
}
// 편의 메서드들
getHotIssues(timeframe = 'today', limit = 10) {
return this.request(`/hot-issues?timeframe=${timeframe}&limit=${limit}`);
}
getPlayerNews(playerName, timeframe = '7days') {
return this.request(`/player-news?player_name=${encodeURIComponent(playerName)}&timeframe=${timeframe}`);
}
searchTweets(keywords, timeframe = '7days', limit = 20) {
return this.request('/search-tweets', {
method: 'POST',
body: JSON.stringify({ keywords, timeframe, limit })
});
}
getCategoryTrends(category, timeframe = '7days') {
return this.request(`/category-trends/${category}?timeframe=${timeframe}`);
}
}
// 사용법
const client = new EPLMCPClient('https://your-api-domain.com/api');
const hotIssues = await client.getHotIssues('week', 15);
```
### 2. React Hook
```typescript
// useEPLData.js
import { useState, useEffect } from 'react';
export const useEPLData = (endpoint, params = {}) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
const client = new EPLMCPClient();
const result = await client.request(endpoint, { params });
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
}, [endpoint, JSON.stringify(params)]);
return { data, loading, error };
};
// 사용법
const HotIssuesComponent = () => {
const { data, loading, error } = useEPLData('/hot-issues', { timeframe: 'today' });
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
{data?.hot_issues?.map(issue => (
<div key={issue.id}>{issue.content}</div>
))}
</div>
);
};
```
## 결론
이제 EPL MCP Server를 다음 두 가지 방식으로 사용할 수 있습니다:
### 1. Claude Desktop (MCP Protocol)
- 직접적인 도구 연동
- 자연어 질의 가능
- AI 모델과의 원활한 상호작용
### 2. 웹 API (HTTP REST)
- 웹 애플리케이션에서 사용
- 모바일 앱 개발 가능
- 다양한 프론트엔드 프레임워크 연동
- 서드파티 통합 가능
두 방식 모두 동일한 Supabase 데이터베이스를 사용하므로 데이터 일관성이 보장되며, 용도에 따라 적절한 방식을 선택하여 사용할 수 있습니다.