feat(docs): enhance SKILL.md and add free tools documentation
- Updated SKILL.md to include a new description emphasizing free tools available without a token. - Added detailed documentation for two free tools: Get News Categories and Get Hot News, including usage examples and response structures. - Introduced a new FreeNewsAPIClient class to handle requests for free API endpoints. - Updated app context to manage both free and premium API clients. - Implemented token requirement checks for premium tools and ensured free tools are accessible without authentication.
This commit is contained in:
@@ -1,15 +1,15 @@
|
||||
---
|
||||
name: opennews
|
||||
description: "Real-time crypto & financial news aggregator — 72+ data sources across 5 categories (News: Bloomberg, Reuters, FT, CNBC, CoinDesk, Twitter/X + 47 more; Listing: Binance, Coinbase, OKX + 6 more; OnChain: whale & KOL trades; Meme: social sentiment; Market: price/funding/liquidation alerts). AI-analyzed with impact score, trading signals, and bilingual summaries."
|
||||
description: "Real-time crypto & financial news aggregator — 72+ data sources across 5 categories (News: Bloomberg, Reuters, FT, CNBC, CoinDesk, Twitter/X + 47 more; Listing: Binance, Coinbase, OKX + 6 more; OnChain: whale & KOL trades; Meme: social sentiment; Market: price/funding/liquidation alerts). AI-analyzed with impact score, trading signals, and bilingual summaries. **Free tools available without token**."
|
||||
|
||||
user-invocable: true
|
||||
metadata:
|
||||
openclaw:
|
||||
requires:
|
||||
env:
|
||||
- OPENNEWS_TOKEN
|
||||
bins:
|
||||
- curl
|
||||
optionalEnv:
|
||||
- OPENNEWS_TOKEN
|
||||
primaryEnv: OPENNEWS_TOKEN
|
||||
emoji: "\U0001F4F0"
|
||||
install:
|
||||
@@ -21,7 +21,7 @@ metadata:
|
||||
- darwin
|
||||
- linux
|
||||
- win32
|
||||
version: 1.0.1
|
||||
version: 1.0.2
|
||||
---
|
||||
|
||||
# OpenNews Crypto News Skill
|
||||
@@ -42,6 +42,86 @@ Real-time crypto & financial news aggregator powered by 6551.io — **72+ data s
|
||||
| **Meme** | 1 | Twitter meme coin social sentiment |
|
||||
| **Market** | 6 | Price Change, Funding Rate, Funding Rate Difference, Large Liquidation, Market Trends, OI Change |
|
||||
|
||||
## Free Tools (No Token Required)
|
||||
|
||||
Two tools are available **without** an API token:
|
||||
|
||||
### 1. Get News Categories
|
||||
|
||||
Get all available news categories and subcategories.
|
||||
|
||||
```bash
|
||||
curl -s "https://ai.6551.io/open/free_categories"
|
||||
```
|
||||
|
||||
### 2. Get Hot News
|
||||
|
||||
Get hot news articles and trending tweets by category.
|
||||
|
||||
```bash
|
||||
curl -s "https://ai.6551.io/open/free_hot?category=macro"
|
||||
```
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| category | string | Yes | Category key from free_categories (e.g., `macro`, `crypto`, `defi`) |
|
||||
| subcategory | string | No | Subcategory key for filtered results |
|
||||
|
||||
**Example categories:**
|
||||
- `macro` - Macro economics and policy
|
||||
- `crypto` - Cryptocurrency news
|
||||
- `blockchain` - Blockchain technology
|
||||
- `defi` - DeFi updates
|
||||
- `nft` - NFT news
|
||||
- `market` - Market analysis
|
||||
|
||||
**Response structure:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"category": "crypto",
|
||||
"subcategory": "defi",
|
||||
"news": {
|
||||
"success": true,
|
||||
"count": 10,
|
||||
"items": [
|
||||
{
|
||||
"id": 123,
|
||||
"title": "DeFi Protocol Reaches $1B TVL",
|
||||
"source": "CoinDesk",
|
||||
"link": "https://...",
|
||||
"score": 85,
|
||||
"grade": "A",
|
||||
"signal": "bullish",
|
||||
"summary_zh": "...",
|
||||
"summary_en": "...",
|
||||
"coins": ["BTC", "ETH"],
|
||||
"published_at": "2026-03-17T10:00:00Z"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tweets": {
|
||||
"success": true,
|
||||
"count": 5,
|
||||
"items": [
|
||||
{
|
||||
"author": "Vitalik Buterin",
|
||||
"handle": "VitalikButerin",
|
||||
"content": "...",
|
||||
"url": "https://...",
|
||||
"metrics": { "likes": 1000, "retweets": 200, "replies": 50 },
|
||||
"posted_at": "2026-03-17T09:00:00Z",
|
||||
"relevance": "high"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Get API Token for Full Access
|
||||
|
||||
To access all 72+ sources with AI analysis, trading signals, and real-time WebSocket updates, get your API token at https://6551.io/mcp
|
||||
|
||||
## Authentication
|
||||
|
||||
All requests require the header:
|
||||
@@ -151,7 +231,26 @@ Important: You need to understand the user's query intent and perform word segme
|
||||
|
||||
## Common Workflows
|
||||
|
||||
### Quick Market Overview
|
||||
### Free Tools (No Token Required)
|
||||
|
||||
**Get all categories:**
|
||||
```bash
|
||||
curl -s "https://ai.6551.io/open/free_categories"
|
||||
```
|
||||
|
||||
**Get hot crypto news:**
|
||||
```bash
|
||||
curl -s "https://ai.6551.io/open/free_hot?category=macro"
|
||||
```
|
||||
|
||||
**Get DeFi subcategory news:**
|
||||
```bash
|
||||
curl -s "https://ai.6551.io/open/free_hot?category=macro&subcategory=defi"
|
||||
```
|
||||
|
||||
### Premium Tools (Requires API Token)
|
||||
|
||||
**Quick Market Overview:**
|
||||
```bash
|
||||
curl -s -X POST "https://ai.6551.io/open/news_search" \
|
||||
-H "Authorization: Bearer $OPENNEWS_TOKEN" \
|
||||
@@ -169,6 +268,24 @@ curl -s -X POST "https://ai.6551.io/open/news_search" \
|
||||
|
||||
## Notes
|
||||
|
||||
- Get your API token at https://6551.io/mcp
|
||||
### Free vs. Premium Features
|
||||
|
||||
**Free (no token required):**
|
||||
- `get_news_categories` - Browse available news categories
|
||||
- `get_hot_news` - Get hot news by category
|
||||
|
||||
**Premium (requires API token from https://6551.io/mcp):**
|
||||
- Full access to 72+ data sources
|
||||
- AI analysis with impact scores (0-100), grades (A-F), and trading signals (long/short/neutral)
|
||||
- Advanced search by keyword, coin, source, or engine type
|
||||
- Real-time WebSocket news streaming
|
||||
- Bilingual summaries (English & Chinese)
|
||||
- Market anomaly signals (liquidations, funding rates, OI changes)
|
||||
|
||||
### API Limits
|
||||
|
||||
- Rate limits apply; max 100 results per request
|
||||
- AI ratings may not be available on all articles (check `status == "done"`)
|
||||
- **Free API data is cached and updated periodically** - not real-time
|
||||
- If data is still being generated, a 503 response may be returned
|
||||
- Get your API token at https://6551.io/mcp for full access
|
||||
|
||||
@@ -15,6 +15,65 @@ logger = logging.getLogger(__name__)
|
||||
MAX_RETRIES = 2
|
||||
|
||||
|
||||
class FreeNewsAPIClient:
|
||||
"""Async HTTP client for free (no-auth) 6551 news API endpoints."""
|
||||
|
||||
def __init__(self, base_url: str = API_BASE_URL):
|
||||
self.base_url = base_url.rstrip("/")
|
||||
self._client: Optional[httpx.AsyncClient] = None
|
||||
|
||||
async def _get_client(self) -> httpx.AsyncClient:
|
||||
if self._client is None or self._client.is_closed:
|
||||
self._client = httpx.AsyncClient(timeout=httpx.Timeout(30.0))
|
||||
return self._client
|
||||
|
||||
async def _reset_client(self):
|
||||
"""Force close and recreate the HTTP client."""
|
||||
if self._client and not self._client.is_closed:
|
||||
await self._client.aclose()
|
||||
self._client = None
|
||||
|
||||
async def close(self):
|
||||
await self._reset_client()
|
||||
|
||||
async def _request(self, method: str, url: str, **kwargs) -> httpx.Response:
|
||||
"""Execute an HTTP request with automatic retry on connection errors."""
|
||||
last_exc = None
|
||||
for attempt in range(MAX_RETRIES + 1):
|
||||
try:
|
||||
client = await self._get_client()
|
||||
resp = await client.request(method, url, **kwargs)
|
||||
resp.raise_for_status()
|
||||
return resp
|
||||
except (httpx.ConnectError, httpx.RemoteProtocolError) as e:
|
||||
last_exc = e
|
||||
logger.warning(
|
||||
"Connection error (attempt %d/%d): %s",
|
||||
attempt + 1, MAX_RETRIES + 1, repr(e),
|
||||
)
|
||||
await self._reset_client()
|
||||
except httpx.HTTPStatusError:
|
||||
raise
|
||||
raise last_exc # type: ignore[misc]
|
||||
|
||||
async def get_free_categories(self) -> dict:
|
||||
"""GET /open/free_categories — 获取所有新闻分类"""
|
||||
resp = await self._request("GET", f"{self.base_url}/open/free_categories")
|
||||
return resp.json()
|
||||
|
||||
async def get_free_hot(
|
||||
self,
|
||||
category: str,
|
||||
subcategory: str = "",
|
||||
) -> dict:
|
||||
"""GET /open/free_hot — 获取热点新闻和推文"""
|
||||
params: dict = {"category": category}
|
||||
if subcategory:
|
||||
params["subcategory"] = subcategory
|
||||
resp = await self._request("GET", f"{self.base_url}/open/free_hot", params=params)
|
||||
return resp.json()
|
||||
|
||||
|
||||
class NewsAPIClient:
|
||||
"""Async HTTP client for the 6551 news REST API."""
|
||||
|
||||
|
||||
+14
-8
@@ -7,7 +7,8 @@ from pathlib import Path
|
||||
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
from opennews_mcp.api_client import NewsAPIClient, NewsWSClient
|
||||
from opennews_mcp.api_client import FreeNewsAPIClient, NewsAPIClient, NewsWSClient
|
||||
from opennews_mcp.config import HAS_TOKEN
|
||||
|
||||
# Knowledge directory (project root / knowledge)
|
||||
KNOWLEDGE_DIR = Path(__file__).resolve().parent.parent.parent / "knowledge"
|
||||
@@ -16,20 +17,25 @@ KNOWLEDGE_DIR = Path(__file__).resolve().parent.parent.parent / "knowledge"
|
||||
@dataclass
|
||||
class AppContext:
|
||||
"""Shared application state available to all tools via ctx."""
|
||||
api: NewsAPIClient
|
||||
ws: NewsWSClient
|
||||
free_api: FreeNewsAPIClient
|
||||
api: NewsAPIClient | None = None
|
||||
ws: NewsWSClient | None = None
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]:
|
||||
"""Manage the API client lifecycle."""
|
||||
api = NewsAPIClient()
|
||||
ws = NewsWSClient()
|
||||
free_api = FreeNewsAPIClient()
|
||||
api = NewsAPIClient() if HAS_TOKEN else None
|
||||
ws = NewsWSClient() if HAS_TOKEN else None
|
||||
try:
|
||||
yield AppContext(api=api, ws=ws)
|
||||
yield AppContext(free_api=free_api, api=api, ws=ws)
|
||||
finally:
|
||||
await api.close()
|
||||
await ws.close()
|
||||
await free_api.close()
|
||||
if api:
|
||||
await api.close()
|
||||
if ws:
|
||||
await ws.close()
|
||||
|
||||
|
||||
# ---------- FastMCP instance ----------
|
||||
|
||||
@@ -24,12 +24,24 @@ API_BASE_URL = os.environ.get("OPENNEWS_API_BASE") or _cfg.get("api_base_url", "
|
||||
WSS_URL = os.environ.get("OPENNEWS_WSS_URL") or _cfg.get("wss_url", "")
|
||||
API_TOKEN = os.environ.get("OPENNEWS_TOKEN") or _cfg.get("api_token", "")
|
||||
|
||||
# 检查 token 是否配置
|
||||
if not API_TOKEN:
|
||||
raise ValueError(
|
||||
"OPENNEWS_TOKEN 未配置。请前往 https://6551.io/mcp 申请 API Token,"
|
||||
"然后设置环境变量 OPENNEWS_TOKEN 或在 config.json 中配置 api_token。"
|
||||
)
|
||||
# Token 状态检查(可选)
|
||||
HAS_TOKEN = bool(API_TOKEN)
|
||||
|
||||
TOKEN_REQUIRED_MSG = (
|
||||
"此工具需要 API Token 才能使用。\n"
|
||||
"请前往 https://6551.io/mcp 免费申请 Token,\n"
|
||||
"然后设置环境变量 OPENNEWS_TOKEN 或在 config.json 中填写 api_token。\n\n"
|
||||
"This tool requires an API token. "
|
||||
"Get your free token at https://6551.io/mcp, "
|
||||
"then set OPENNEWS_TOKEN env var or add api_token to config.json."
|
||||
)
|
||||
|
||||
|
||||
def require_token() -> dict | None:
|
||||
"""检查是否配置了 token,未配置时返回错误信息。"""
|
||||
if not HAS_TOKEN:
|
||||
return {"success": False, "error": TOKEN_REQUIRED_MSG}
|
||||
return None
|
||||
|
||||
# ---------- Safety ----------
|
||||
MAX_ROWS = int(os.environ.get("OPENNEWS_MAX_ROWS", 0) or _cfg.get("max_rows", 100))
|
||||
|
||||
@@ -4,5 +4,6 @@ Each module is self-contained and atomic — no inter-tool dependencies.
|
||||
"""
|
||||
|
||||
from opennews_mcp.tools import discovery # noqa: F401
|
||||
from opennews_mcp.tools import free # noqa: F401
|
||||
from opennews_mcp.tools import news # noqa: F401
|
||||
from opennews_mcp.tools import realtime # noqa: F401
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from mcp.server.fastmcp import Context
|
||||
|
||||
from opennews_mcp.app import mcp
|
||||
from opennews_mcp.config import require_token
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
@@ -31,6 +32,8 @@ async def get_news_sources(ctx: Context) -> dict:
|
||||
Returns a tree structure with all engine types and their sub-categories.
|
||||
Use this first to discover what sources are available before searching.
|
||||
"""
|
||||
if (err := require_token()):
|
||||
return err
|
||||
api = ctx.request_context.lifespan_context.api
|
||||
|
||||
try:
|
||||
@@ -72,6 +75,8 @@ async def list_news_types(ctx: Context) -> dict:
|
||||
Returns a flat list of news source codes that can be used with
|
||||
the newsType parameter in search_news.
|
||||
"""
|
||||
if (err := require_token()):
|
||||
return err
|
||||
# See get_news_sources for the full 72+ source catalog.
|
||||
api = ctx.request_context.lifespan_context.api
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
"""Free news tools — no token required, access via /open/free_* endpoints.
|
||||
|
||||
These tools provide basic news access without authentication.
|
||||
For full features (72+ sources, AI analysis, real-time WebSocket), get a free token at https://6551.io/mcp.
|
||||
"""
|
||||
|
||||
from mcp.server.fastmcp import Context
|
||||
|
||||
from opennews_mcp.app import mcp
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def get_news_categories(ctx: Context) -> dict:
|
||||
"""Get all available news categories and subcategories.
|
||||
|
||||
Returns a list of categories, each containing subcategories,
|
||||
for use with the get_hot_news tool.
|
||||
|
||||
This is a free tool that does not require an API token.
|
||||
"""
|
||||
free_api = ctx.request_context.lifespan_context.free_api
|
||||
try:
|
||||
result = await free_api.get_free_categories()
|
||||
return {"success": True, "data": result}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e) or repr(e)}
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def get_hot_news(
|
||||
category: str,
|
||||
ctx: Context,
|
||||
subcategory: str = "",
|
||||
) -> dict:
|
||||
"""Get hot news and tweets by category.
|
||||
|
||||
Args:
|
||||
category: Category key (required). Use get_news_categories to list available keys.
|
||||
subcategory: Subcategory key (optional).
|
||||
|
||||
Returns:
|
||||
Combined news articles and tweets for the given category.
|
||||
|
||||
This is a free tool that does not require an API token.
|
||||
"""
|
||||
free_api = ctx.request_context.lifespan_context.free_api
|
||||
try:
|
||||
result = await free_api.get_free_hot(
|
||||
category=category,
|
||||
subcategory=subcategory,
|
||||
)
|
||||
return {"success": True, "data": result}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e) or repr(e)}
|
||||
@@ -8,7 +8,7 @@ Uses POST /open/news_search as the primary data source.
|
||||
from mcp.server.fastmcp import Context
|
||||
|
||||
from opennews_mcp.app import mcp
|
||||
from opennews_mcp.config import clamp_limit, make_serializable, MAX_ROWS
|
||||
from opennews_mcp.config import clamp_limit, make_serializable, MAX_ROWS, require_token
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
@@ -21,6 +21,8 @@ async def get_latest_news(ctx: Context, limit: int = 10) -> dict:
|
||||
Args:
|
||||
limit: Maximum number of articles to return (default 10, max 100).
|
||||
"""
|
||||
if (err := require_token()):
|
||||
return err
|
||||
api = ctx.request_context.lifespan_context.api
|
||||
limit = clamp_limit(limit)
|
||||
try:
|
||||
@@ -45,6 +47,8 @@ async def search_news(keyword: str, ctx: Context, limit: int = 10) -> dict:
|
||||
keyword: Search term (e.g. "bitcoin", "SEC", "ETF").
|
||||
limit: Maximum results (default 10, max 100).
|
||||
"""
|
||||
if (err := require_token()):
|
||||
return err
|
||||
api = ctx.request_context.lifespan_context.api
|
||||
limit = clamp_limit(limit)
|
||||
try:
|
||||
@@ -69,6 +73,8 @@ async def search_news_by_coin(coin: str, ctx: Context, limit: int = 10) -> dict:
|
||||
coin: Coin symbol or name (e.g. "BTC", "ETH", "SOL", "TRUMP").
|
||||
limit: Maximum results (default 10, max 100).
|
||||
"""
|
||||
if (err := require_token()):
|
||||
return err
|
||||
api = ctx.request_context.lifespan_context.api
|
||||
limit = clamp_limit(limit)
|
||||
try:
|
||||
@@ -99,6 +105,8 @@ async def get_news_by_source(engine_type: str, news_type: str, ctx: Context, lim
|
||||
"Large Liquidation", "Market Trends", "OI Change".
|
||||
limit: Maximum results (default 10, max 100).
|
||||
"""
|
||||
if (err := require_token()):
|
||||
return err
|
||||
api = ctx.request_context.lifespan_context.api
|
||||
limit = clamp_limit(limit)
|
||||
try:
|
||||
@@ -127,6 +135,8 @@ async def get_news_by_engine(engine_type: str, ctx: Context, limit: int = 10) ->
|
||||
engine_type: The engine type code.
|
||||
limit: Maximum results (default 10, max 100).
|
||||
"""
|
||||
if (err := require_token()):
|
||||
return err
|
||||
api = ctx.request_context.lifespan_context.api
|
||||
limit = clamp_limit(limit)
|
||||
try:
|
||||
@@ -161,6 +171,8 @@ async def search_news_advanced(
|
||||
has_coin: If true, only return news that have associated coins.
|
||||
limit: Maximum results (default 10, max 100).
|
||||
"""
|
||||
if (err := require_token()):
|
||||
return err
|
||||
api = ctx.request_context.lifespan_context.api
|
||||
limit = clamp_limit(limit)
|
||||
|
||||
@@ -203,6 +215,8 @@ async def get_high_score_news(ctx: Context, min_score: int = 70, limit: int = 10
|
||||
min_score: Minimum score threshold (default 70).
|
||||
limit: Maximum results to return (default 10, max 100).
|
||||
"""
|
||||
if (err := require_token()):
|
||||
return err
|
||||
api = ctx.request_context.lifespan_context.api
|
||||
limit = clamp_limit(limit)
|
||||
try:
|
||||
@@ -235,6 +249,8 @@ async def get_news_by_signal(signal: str, ctx: Context, limit: int = 10) -> dict
|
||||
signal: The signal type: "long" (bullish), "short" (bearish), or "neutral".
|
||||
limit: Maximum results (default 10, max 100).
|
||||
"""
|
||||
if (err := require_token()):
|
||||
return err
|
||||
api = ctx.request_context.lifespan_context.api
|
||||
limit = clamp_limit(limit)
|
||||
try:
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from mcp.server.fastmcp import Context
|
||||
|
||||
from opennews_mcp.app import mcp
|
||||
from opennews_mcp.config import make_serializable
|
||||
from opennews_mcp.config import make_serializable, require_token
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
@@ -31,6 +31,8 @@ async def subscribe_latest_news(
|
||||
engine_types: Engine type filter in format "type1:cat1,cat2;type2:cat3".
|
||||
has_coin: If true, only receive news that have associated coins.
|
||||
"""
|
||||
if (err := require_token()):
|
||||
return err
|
||||
ws = ctx.request_context.lifespan_context.ws
|
||||
wait_seconds = min(max(1, wait_seconds), 30)
|
||||
max_items = min(max(1, max_items), 20)
|
||||
@@ -48,7 +50,7 @@ async def subscribe_latest_news(
|
||||
engine_types_dict[engine] = cat_list
|
||||
|
||||
try:
|
||||
sub_result = await ws.subscribe_latest(
|
||||
await ws.subscribe_latest(
|
||||
engine_types=engine_types_dict,
|
||||
coins=coin_list,
|
||||
has_coin=has_coin,
|
||||
|
||||
Reference in New Issue
Block a user