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:
beare
2026-03-30 11:59:56 +08:00
parent ce08698e11
commit f38eb1fd85
9 changed files with 295 additions and 23 deletions
+123 -6
View File
@@ -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
+59
View File
@@ -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
View File
@@ -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 ----------
+18 -6
View File
@@ -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))
+1
View File
@@ -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
+5
View File
@@ -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
+54
View File
@@ -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)}
+17 -1
View File
@@ -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:
+4 -2
View File
@@ -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,