crewai-webzio 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- crewai_webzio-0.1.0/.env.example +2 -0
- crewai_webzio-0.1.0/.gitignore +19 -0
- crewai_webzio-0.1.0/LICENSE +21 -0
- crewai_webzio-0.1.0/PKG-INFO +144 -0
- crewai_webzio-0.1.0/PUBLISHING.md +36 -0
- crewai_webzio-0.1.0/README.md +117 -0
- crewai_webzio-0.1.0/crewai_webzio/__init__.py +29 -0
- crewai_webzio-0.1.0/crewai_webzio/consts.py +5 -0
- crewai_webzio-0.1.0/crewai_webzio/tool.py +161 -0
- crewai_webzio-0.1.0/examples/run_news_search.py +40 -0
- crewai_webzio-0.1.0/pyproject.toml +48 -0
- crewai_webzio-0.1.0/tests/test_live.py +27 -0
- crewai_webzio-0.1.0/tests/test_tool.py +156 -0
- crewai_webzio-0.1.0/upstream/README.md +44 -0
- crewai_webzio-0.1.0/upstream/docs/en/tools/search-research/overview-card-snippet.mdx +3 -0
- crewai_webzio-0.1.0/upstream/docs/en/tools/search-research/webzionewssearchtool.mdx +135 -0
- crewai_webzio-0.1.0/upstream/lib/crewai-tools/src/crewai_tools/tools/webzio_tools/__init__.py +0 -0
- crewai_webzio-0.1.0/upstream/lib/crewai-tools/src/crewai_tools/tools/webzio_tools/webzio_news_search_tool.py +155 -0
- crewai_webzio-0.1.0/upstream/lib/crewai-tools/tests/tools/webzio_news_search_tool_test.py +102 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Webz.io
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: crewai-webzio
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CrewAI tools for Webz.io News Search - global news in natural language with semantic ranking and rich filters
|
|
5
|
+
Project-URL: Homepage, https://news-search-mcp.webz.io
|
|
6
|
+
Project-URL: Documentation, https://docs.webz.io/docs/webz/news-search-api-mcp
|
|
7
|
+
Project-URL: Repository, https://github.com/Webhose/webz-news-search
|
|
8
|
+
Project-URL: Issues, https://github.com/Webhose/webz-news-search/issues
|
|
9
|
+
Author-email: "Webz.io" <support@webz.io>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: crewai,mcp,news,search,webz,webzio
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Requires-Dist: crewai-tools[mcp]>=0.40.0
|
|
23
|
+
Requires-Dist: crewai>=0.80.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# crewai-webzio
|
|
29
|
+
|
|
30
|
+
**Search global news with [Webz.io](https://webz.io) from [CrewAI](https://crewai.com) agents — in natural language, with the most relevant articles first.**
|
|
31
|
+
|
|
32
|
+
[Webz.io News Search](https://docs.webz.io/docs/webz/news-search-api-mcp) covers news and current events from sources worldwide. Ask a question in plain language, narrow results with filters (language, country, date, sentiment, domain, ticker, and more), and get back focused article excerpts with titles, URLs, and metadata.
|
|
33
|
+
|
|
34
|
+
Use this package with CrewAI agents, or construct the tool directly to verify MCP connectivity.
|
|
35
|
+
|
|
36
|
+
## What you get
|
|
37
|
+
|
|
38
|
+
- **Natural-language search** — no keyword hacking. Example: `"EU AI Act enforcement updates"` or `"How is Tesla stock reacting to earnings?"`
|
|
39
|
+
- **Worldwide coverage** — semantic search over Webz.io's global news index.
|
|
40
|
+
- **Rich filters** — language, country, days, sentiment, domain, ticker, person, organization, topic, and more. See the [MCP tool reference](https://docs.webz.io/docs/webz/news-search-api-mcp#tool-reference).
|
|
41
|
+
- **Live schema** — filters are loaded from the hosted MCP server (`tools/list`). New Webz filters appear automatically, without republishing this package.
|
|
42
|
+
- **CrewAI tool** — pass `WebzioNewsSearchTool(...)` to `Agent(tools=[...])`.
|
|
43
|
+
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install crewai-webzio
|
|
48
|
+
export WEBZ_API_TOKEN="your-webz-api-token"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Get a token from your [Webz.io dashboard](https://webz.io) (same token as the News Search API).
|
|
52
|
+
|
|
53
|
+
Requires CrewAI with MCP support (`crewai-tools[mcp]`, installed automatically).
|
|
54
|
+
|
|
55
|
+
Full setup and client options: [MCP Server docs](https://docs.webz.io/docs/webz/news-search-api-mcp).
|
|
56
|
+
|
|
57
|
+
## With a CrewAI agent
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
import os
|
|
61
|
+
|
|
62
|
+
from crewai import Agent, Crew, Task
|
|
63
|
+
from crewai_webzio import WebzioNewsSearchTool
|
|
64
|
+
|
|
65
|
+
with WebzioNewsSearchTool(api_token=os.environ["WEBZ_API_TOKEN"]) as news_tool:
|
|
66
|
+
researcher = Agent(
|
|
67
|
+
role="Research Analyst",
|
|
68
|
+
goal="Find recent news on any topic",
|
|
69
|
+
backstory="Expert researcher with access to Webz.io news search.",
|
|
70
|
+
tools=[news_tool],
|
|
71
|
+
verbose=True,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
research_task = Task(
|
|
75
|
+
description=(
|
|
76
|
+
"Search Webz news for recent Nvidia supply-chain risk coverage "
|
|
77
|
+
"and summarize with sources."
|
|
78
|
+
),
|
|
79
|
+
expected_output="A summary with headline, publisher, and URL for each article.",
|
|
80
|
+
agent=researcher,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
crew = Crew(agents=[researcher], tasks=[research_task], verbose=True)
|
|
84
|
+
print(crew.kickoff())
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Use the tool as a context manager (or call `stop()` when done) to shut down the MCP session.
|
|
88
|
+
|
|
89
|
+
## Direct search (no agent required)
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
export WEBZ_API_TOKEN="your-token"
|
|
93
|
+
python examples/run_news_search.py
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from crewai_webzio import WebzioNewsSearchTool
|
|
98
|
+
|
|
99
|
+
with WebzioNewsSearchTool() as tool:
|
|
100
|
+
print(sorted(tool.arg_names)) # live filters from MCP tools/list
|
|
101
|
+
print(tool._run(query="EU AI regulation", k=5, days=30))
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Using Webz via MCP (no package install)
|
|
105
|
+
|
|
106
|
+
CrewAI can connect to the hosted MCP server directly:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from crewai import Agent
|
|
110
|
+
from crewai.mcp import MCPServerHTTP
|
|
111
|
+
|
|
112
|
+
agent = Agent(
|
|
113
|
+
role="News Researcher",
|
|
114
|
+
goal="Find and summarize current news",
|
|
115
|
+
mcps=[
|
|
116
|
+
MCPServerHTTP(
|
|
117
|
+
url="https://news-search-mcp.webz.io/mcp",
|
|
118
|
+
headers={"Authorization": "Bearer YOUR_WEBZ_API_TOKEN"},
|
|
119
|
+
),
|
|
120
|
+
],
|
|
121
|
+
)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## How it works
|
|
125
|
+
|
|
126
|
+
This package wraps CrewAI's [`MCPServerAdapter`](https://docs.crewai.com/en/mcp/overview) around the hosted Webz News Search MCP server at `https://news-search-mcp.webz.io/mcp`. Each tool call runs a regular News Search API request with your token (same credits and rate limits). Filter fields are not hardcoded — the tool schema comes from the live server.
|
|
127
|
+
|
|
128
|
+
## Configuration
|
|
129
|
+
|
|
130
|
+
| Name | Default | Purpose |
|
|
131
|
+
| --- | --- | --- |
|
|
132
|
+
| `WEBZ_API_TOKEN` | required | Webz API token from the dashboard |
|
|
133
|
+
| `WEBZ_MCP_URL` | `https://news-search-mcp.webz.io/mcp` | Override for local MCP testing |
|
|
134
|
+
|
|
135
|
+
You can also pass `api_token=` and `mcp_url=` to `WebzioNewsSearchTool()`.
|
|
136
|
+
|
|
137
|
+
## Links
|
|
138
|
+
|
|
139
|
+
- [Webz.io](https://webz.io)
|
|
140
|
+
- [News Search MCP documentation](https://docs.webz.io/docs/webz/news-search-api-mcp)
|
|
141
|
+
- [MCP server landing page](https://news-search-mcp.webz.io)
|
|
142
|
+
- [News Search API filters](https://docs.webz.io/docs/webz/news-search-api-filters)
|
|
143
|
+
- [CrewAI MCP overview](https://docs.crewai.com/en/mcp/overview)
|
|
144
|
+
- [GitHub](https://github.com/Webhose/webz-news-search)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Publishing crewai-webzio to PyPI
|
|
2
|
+
|
|
3
|
+
## Prerequisites
|
|
4
|
+
|
|
5
|
+
- PyPI account and API token with upload scope
|
|
6
|
+
- `twine` installed (`pip install twine build`)
|
|
7
|
+
|
|
8
|
+
## Build
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
cd packages/crewai
|
|
12
|
+
python -m pip install build
|
|
13
|
+
python -m build
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Artifacts land in `dist/`:
|
|
17
|
+
|
|
18
|
+
- `crewai_webzio-0.1.0-py3-none-any.whl`
|
|
19
|
+
- `crewai_webzio-0.1.0.tar.gz`
|
|
20
|
+
|
|
21
|
+
## Upload
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
export TWINE_USERNAME=__token__
|
|
25
|
+
export TWINE_PASSWORD=pypi-xxxxxxxx
|
|
26
|
+
python -m twine upload dist/crewai_webzio-*
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Or use `uv publish` if configured.
|
|
30
|
+
|
|
31
|
+
## Verify
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install crewai-webzio
|
|
35
|
+
python -c "from crewai_webzio import WebzioNewsSearchTool; print(WebzioNewsSearchTool)"
|
|
36
|
+
```
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# crewai-webzio
|
|
2
|
+
|
|
3
|
+
**Search global news with [Webz.io](https://webz.io) from [CrewAI](https://crewai.com) agents — in natural language, with the most relevant articles first.**
|
|
4
|
+
|
|
5
|
+
[Webz.io News Search](https://docs.webz.io/docs/webz/news-search-api-mcp) covers news and current events from sources worldwide. Ask a question in plain language, narrow results with filters (language, country, date, sentiment, domain, ticker, and more), and get back focused article excerpts with titles, URLs, and metadata.
|
|
6
|
+
|
|
7
|
+
Use this package with CrewAI agents, or construct the tool directly to verify MCP connectivity.
|
|
8
|
+
|
|
9
|
+
## What you get
|
|
10
|
+
|
|
11
|
+
- **Natural-language search** — no keyword hacking. Example: `"EU AI Act enforcement updates"` or `"How is Tesla stock reacting to earnings?"`
|
|
12
|
+
- **Worldwide coverage** — semantic search over Webz.io's global news index.
|
|
13
|
+
- **Rich filters** — language, country, days, sentiment, domain, ticker, person, organization, topic, and more. See the [MCP tool reference](https://docs.webz.io/docs/webz/news-search-api-mcp#tool-reference).
|
|
14
|
+
- **Live schema** — filters are loaded from the hosted MCP server (`tools/list`). New Webz filters appear automatically, without republishing this package.
|
|
15
|
+
- **CrewAI tool** — pass `WebzioNewsSearchTool(...)` to `Agent(tools=[...])`.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install crewai-webzio
|
|
21
|
+
export WEBZ_API_TOKEN="your-webz-api-token"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Get a token from your [Webz.io dashboard](https://webz.io) (same token as the News Search API).
|
|
25
|
+
|
|
26
|
+
Requires CrewAI with MCP support (`crewai-tools[mcp]`, installed automatically).
|
|
27
|
+
|
|
28
|
+
Full setup and client options: [MCP Server docs](https://docs.webz.io/docs/webz/news-search-api-mcp).
|
|
29
|
+
|
|
30
|
+
## With a CrewAI agent
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
import os
|
|
34
|
+
|
|
35
|
+
from crewai import Agent, Crew, Task
|
|
36
|
+
from crewai_webzio import WebzioNewsSearchTool
|
|
37
|
+
|
|
38
|
+
with WebzioNewsSearchTool(api_token=os.environ["WEBZ_API_TOKEN"]) as news_tool:
|
|
39
|
+
researcher = Agent(
|
|
40
|
+
role="Research Analyst",
|
|
41
|
+
goal="Find recent news on any topic",
|
|
42
|
+
backstory="Expert researcher with access to Webz.io news search.",
|
|
43
|
+
tools=[news_tool],
|
|
44
|
+
verbose=True,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
research_task = Task(
|
|
48
|
+
description=(
|
|
49
|
+
"Search Webz news for recent Nvidia supply-chain risk coverage "
|
|
50
|
+
"and summarize with sources."
|
|
51
|
+
),
|
|
52
|
+
expected_output="A summary with headline, publisher, and URL for each article.",
|
|
53
|
+
agent=researcher,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
crew = Crew(agents=[researcher], tasks=[research_task], verbose=True)
|
|
57
|
+
print(crew.kickoff())
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Use the tool as a context manager (or call `stop()` when done) to shut down the MCP session.
|
|
61
|
+
|
|
62
|
+
## Direct search (no agent required)
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
export WEBZ_API_TOKEN="your-token"
|
|
66
|
+
python examples/run_news_search.py
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from crewai_webzio import WebzioNewsSearchTool
|
|
71
|
+
|
|
72
|
+
with WebzioNewsSearchTool() as tool:
|
|
73
|
+
print(sorted(tool.arg_names)) # live filters from MCP tools/list
|
|
74
|
+
print(tool._run(query="EU AI regulation", k=5, days=30))
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Using Webz via MCP (no package install)
|
|
78
|
+
|
|
79
|
+
CrewAI can connect to the hosted MCP server directly:
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from crewai import Agent
|
|
83
|
+
from crewai.mcp import MCPServerHTTP
|
|
84
|
+
|
|
85
|
+
agent = Agent(
|
|
86
|
+
role="News Researcher",
|
|
87
|
+
goal="Find and summarize current news",
|
|
88
|
+
mcps=[
|
|
89
|
+
MCPServerHTTP(
|
|
90
|
+
url="https://news-search-mcp.webz.io/mcp",
|
|
91
|
+
headers={"Authorization": "Bearer YOUR_WEBZ_API_TOKEN"},
|
|
92
|
+
),
|
|
93
|
+
],
|
|
94
|
+
)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## How it works
|
|
98
|
+
|
|
99
|
+
This package wraps CrewAI's [`MCPServerAdapter`](https://docs.crewai.com/en/mcp/overview) around the hosted Webz News Search MCP server at `https://news-search-mcp.webz.io/mcp`. Each tool call runs a regular News Search API request with your token (same credits and rate limits). Filter fields are not hardcoded — the tool schema comes from the live server.
|
|
100
|
+
|
|
101
|
+
## Configuration
|
|
102
|
+
|
|
103
|
+
| Name | Default | Purpose |
|
|
104
|
+
| --- | --- | --- |
|
|
105
|
+
| `WEBZ_API_TOKEN` | required | Webz API token from the dashboard |
|
|
106
|
+
| `WEBZ_MCP_URL` | `https://news-search-mcp.webz.io/mcp` | Override for local MCP testing |
|
|
107
|
+
|
|
108
|
+
You can also pass `api_token=` and `mcp_url=` to `WebzioNewsSearchTool()`.
|
|
109
|
+
|
|
110
|
+
## Links
|
|
111
|
+
|
|
112
|
+
- [Webz.io](https://webz.io)
|
|
113
|
+
- [News Search MCP documentation](https://docs.webz.io/docs/webz/news-search-api-mcp)
|
|
114
|
+
- [MCP server landing page](https://news-search-mcp.webz.io)
|
|
115
|
+
- [News Search API filters](https://docs.webz.io/docs/webz/news-search-api-filters)
|
|
116
|
+
- [CrewAI MCP overview](https://docs.crewai.com/en/mcp/overview)
|
|
117
|
+
- [GitHub](https://github.com/Webhose/webz-news-search)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from crewai_webzio.consts import (
|
|
2
|
+
DEFAULT_MCP_URL,
|
|
3
|
+
MCP_TRANSPORT,
|
|
4
|
+
MCP_URL_ENV_NAME,
|
|
5
|
+
PREFERRED_TOOL_NAME,
|
|
6
|
+
TOKEN_ENV_NAME,
|
|
7
|
+
)
|
|
8
|
+
from crewai_webzio.tool import (
|
|
9
|
+
WebzConfigError,
|
|
10
|
+
WebzioNewsSearchTool,
|
|
11
|
+
build_server_params,
|
|
12
|
+
pick_news_search_tool,
|
|
13
|
+
resolve_api_token,
|
|
14
|
+
resolve_mcp_url,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"DEFAULT_MCP_URL",
|
|
19
|
+
"MCP_TRANSPORT",
|
|
20
|
+
"MCP_URL_ENV_NAME",
|
|
21
|
+
"PREFERRED_TOOL_NAME",
|
|
22
|
+
"TOKEN_ENV_NAME",
|
|
23
|
+
"WebzConfigError",
|
|
24
|
+
"WebzioNewsSearchTool",
|
|
25
|
+
"build_server_params",
|
|
26
|
+
"pick_news_search_tool",
|
|
27
|
+
"resolve_api_token",
|
|
28
|
+
"resolve_mcp_url",
|
|
29
|
+
]
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from types import TracebackType
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from crewai.tools import BaseTool, EnvVar
|
|
8
|
+
from crewai_tools import MCPServerAdapter
|
|
9
|
+
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr
|
|
10
|
+
|
|
11
|
+
from crewai_webzio.consts import (
|
|
12
|
+
DEFAULT_MCP_URL,
|
|
13
|
+
MCP_TRANSPORT,
|
|
14
|
+
MCP_URL_ENV_NAME,
|
|
15
|
+
PREFERRED_TOOL_NAME,
|
|
16
|
+
TOKEN_ENV_NAME,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class WebzConfigError(ValueError):
|
|
21
|
+
"""Raised when the Webz MCP client cannot be configured or loaded."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def resolve_api_token(api_token: str | None = None) -> str:
|
|
25
|
+
token = (api_token or os.getenv(TOKEN_ENV_NAME) or "").strip()
|
|
26
|
+
if not token:
|
|
27
|
+
raise WebzConfigError(
|
|
28
|
+
f"missing Webz API token. set {TOKEN_ENV_NAME} or pass api_token."
|
|
29
|
+
)
|
|
30
|
+
return token
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def resolve_mcp_url(mcp_url: str | None = None) -> str:
|
|
34
|
+
url = (mcp_url or os.getenv(MCP_URL_ENV_NAME) or DEFAULT_MCP_URL).strip()
|
|
35
|
+
if not url:
|
|
36
|
+
raise WebzConfigError("missing MCP url.")
|
|
37
|
+
return url.rstrip("/")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def build_server_params(
|
|
41
|
+
api_token: str | None = None,
|
|
42
|
+
*,
|
|
43
|
+
mcp_url: str | None = None,
|
|
44
|
+
) -> dict[str, Any]:
|
|
45
|
+
token = resolve_api_token(api_token)
|
|
46
|
+
url = resolve_mcp_url(mcp_url)
|
|
47
|
+
return {
|
|
48
|
+
"url": url,
|
|
49
|
+
"transport": MCP_TRANSPORT,
|
|
50
|
+
"headers": {"Authorization": f"Bearer {token}"},
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def pick_news_search_tool(tools: list[BaseTool]) -> BaseTool:
|
|
55
|
+
if not tools:
|
|
56
|
+
raise WebzConfigError("MCP server returned no tools.")
|
|
57
|
+
for tool in tools:
|
|
58
|
+
if tool.name == PREFERRED_TOOL_NAME:
|
|
59
|
+
return tool
|
|
60
|
+
if len(tools) == 1:
|
|
61
|
+
return tools[0]
|
|
62
|
+
names = ", ".join(tool.name for tool in tools)
|
|
63
|
+
raise WebzConfigError(
|
|
64
|
+
f"MCP server did not expose {PREFERRED_TOOL_NAME}. available tools: {names}"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class WebzioNewsSearchToolSchema(BaseModel):
|
|
69
|
+
"""Minimal static schema; replaced at init with the live MCP schema."""
|
|
70
|
+
|
|
71
|
+
model_config = ConfigDict(extra="allow")
|
|
72
|
+
|
|
73
|
+
query: str = Field(..., description="Natural-language news search query")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class WebzioNewsSearchTool(BaseTool):
|
|
77
|
+
"""CrewAI tool that exposes Webz.io contextual news search via the hosted MCP server.
|
|
78
|
+
|
|
79
|
+
Filter fields are loaded live from MCP ``tools/list``. New server filters appear
|
|
80
|
+
automatically at runtime without republishing this package.
|
|
81
|
+
|
|
82
|
+
Use as a context manager or call ``stop()`` when done to shut down the MCP session::
|
|
83
|
+
|
|
84
|
+
with WebzioNewsSearchTool() as tool:
|
|
85
|
+
agent = Agent(..., tools=[tool])
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
name: str = PREFERRED_TOOL_NAME
|
|
89
|
+
description: str = (
|
|
90
|
+
"Search global news with Webz.io. Returns article excerpts with titles, "
|
|
91
|
+
"URLs, and metadata. Filter by language, country, date, sentiment, domain, "
|
|
92
|
+
"ticker, and more."
|
|
93
|
+
)
|
|
94
|
+
args_schema: type[BaseModel] = WebzioNewsSearchToolSchema
|
|
95
|
+
package_dependencies: list[str] = Field(
|
|
96
|
+
default_factory=lambda: ["crewai-tools[mcp]"]
|
|
97
|
+
)
|
|
98
|
+
env_vars: list[EnvVar] = Field(
|
|
99
|
+
default_factory=lambda: [
|
|
100
|
+
EnvVar(
|
|
101
|
+
name=TOKEN_ENV_NAME,
|
|
102
|
+
description="Webz.io API token from the dashboard",
|
|
103
|
+
required=True,
|
|
104
|
+
),
|
|
105
|
+
EnvVar(
|
|
106
|
+
name=MCP_URL_ENV_NAME,
|
|
107
|
+
description="Override MCP endpoint for testing",
|
|
108
|
+
required=False,
|
|
109
|
+
),
|
|
110
|
+
]
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
_adapter: MCPServerAdapter | None = PrivateAttr(default=None)
|
|
114
|
+
_live_tool: BaseTool | None = PrivateAttr(default=None)
|
|
115
|
+
|
|
116
|
+
def __init__(
|
|
117
|
+
self,
|
|
118
|
+
api_token: str | None = None,
|
|
119
|
+
*,
|
|
120
|
+
mcp_url: str | None = None,
|
|
121
|
+
connect_timeout: int = 30,
|
|
122
|
+
**kwargs: Any,
|
|
123
|
+
) -> None:
|
|
124
|
+
super().__init__(**kwargs)
|
|
125
|
+
self._adapter = MCPServerAdapter(
|
|
126
|
+
build_server_params(api_token, mcp_url=mcp_url),
|
|
127
|
+
PREFERRED_TOOL_NAME,
|
|
128
|
+
connect_timeout=connect_timeout,
|
|
129
|
+
)
|
|
130
|
+
self._live_tool = pick_news_search_tool(list(self._adapter.tools))
|
|
131
|
+
self.args_schema = self._live_tool.args_schema
|
|
132
|
+
self.description = self._live_tool.description
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def arg_names(self) -> list[str]:
|
|
136
|
+
"""Live argument names from the MCP tool schema."""
|
|
137
|
+
return list(self.args_schema.model_fields.keys())
|
|
138
|
+
|
|
139
|
+
def _run(self, **kwargs: Any) -> str:
|
|
140
|
+
if self._live_tool is None:
|
|
141
|
+
raise WebzConfigError("MCP news search tool is not initialized.")
|
|
142
|
+
result = self._live_tool._run(**kwargs)
|
|
143
|
+
return str(result)
|
|
144
|
+
|
|
145
|
+
def stop(self) -> None:
|
|
146
|
+
"""Stop the underlying MCP server connection."""
|
|
147
|
+
if self._adapter is not None:
|
|
148
|
+
self._adapter.stop()
|
|
149
|
+
self._adapter = None
|
|
150
|
+
self._live_tool = None
|
|
151
|
+
|
|
152
|
+
def __enter__(self) -> WebzioNewsSearchTool:
|
|
153
|
+
return self
|
|
154
|
+
|
|
155
|
+
def __exit__(
|
|
156
|
+
self,
|
|
157
|
+
exc_type: type[BaseException] | None,
|
|
158
|
+
exc_val: BaseException | None,
|
|
159
|
+
exc_tb: TracebackType | None,
|
|
160
|
+
) -> None:
|
|
161
|
+
self.stop()
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Construct a WebzioNewsSearchTool and run one search against the hosted MCP server.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
export WEBZ_API_TOKEN="your-token"
|
|
6
|
+
python examples/run_news_search.py
|
|
7
|
+
|
|
8
|
+
Or pass the token in code with WebzioNewsSearchTool(api_token="...").
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
from crewai_webzio import WebzioNewsSearchTool
|
|
17
|
+
|
|
18
|
+
QUERY = "recent developments on EU AI regulation"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def main() -> None:
|
|
22
|
+
token = os.getenv("WEBZ_API_TOKEN")
|
|
23
|
+
with (
|
|
24
|
+
WebzioNewsSearchTool(api_token=token)
|
|
25
|
+
if token
|
|
26
|
+
else WebzioNewsSearchTool()
|
|
27
|
+
) as tool:
|
|
28
|
+
print("tool name:", tool.name)
|
|
29
|
+
print("live args from MCP tools/list:", sorted(tool.arg_names))
|
|
30
|
+
print("---")
|
|
31
|
+
print(tool._run(query=QUERY, k=3), end="\n\n")
|
|
32
|
+
print("SUCCESS ! tool is ready for Agent(tools=[tool])")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
if __name__ == "__main__":
|
|
36
|
+
try:
|
|
37
|
+
main()
|
|
38
|
+
except Exception as exc:
|
|
39
|
+
print(f"search failed: {exc}", file=sys.stderr)
|
|
40
|
+
raise SystemExit(1)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "crewai-webzio"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "CrewAI tools for Webz.io News Search - global news in natural language with semantic ranking and rich filters"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [
|
|
14
|
+
{ name = "Webz.io", email = "support@webz.io" },
|
|
15
|
+
]
|
|
16
|
+
keywords = ["crewai", "webz", "webzio", "news", "mcp", "search"]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 4 - Beta",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
26
|
+
]
|
|
27
|
+
dependencies = [
|
|
28
|
+
"crewai>=0.80.0",
|
|
29
|
+
"crewai-tools[mcp]>=0.40.0",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
dev = [
|
|
34
|
+
"pytest>=8.0",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://news-search-mcp.webz.io"
|
|
39
|
+
Documentation = "https://docs.webz.io/docs/webz/news-search-api-mcp"
|
|
40
|
+
Repository = "https://github.com/Webhose/webz-news-search"
|
|
41
|
+
Issues = "https://github.com/Webhose/webz-news-search/issues"
|
|
42
|
+
|
|
43
|
+
[tool.hatch.build.targets.wheel]
|
|
44
|
+
packages = ["crewai_webzio"]
|
|
45
|
+
|
|
46
|
+
[tool.pytest.ini_options]
|
|
47
|
+
testpaths = ["tests"]
|
|
48
|
+
pythonpath = ["."]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from crewai_webzio import WebzioNewsSearchTool
|
|
8
|
+
from crewai_webzio.consts import PREFERRED_TOOL_NAME
|
|
9
|
+
|
|
10
|
+
pytestmark = pytest.mark.skipif(
|
|
11
|
+
not os.getenv("WEBZ_API_TOKEN"),
|
|
12
|
+
reason="WEBZ_API_TOKEN is not set",
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_live_tool_discovers_news_search_schema() -> None:
|
|
17
|
+
with WebzioNewsSearchTool() as tool:
|
|
18
|
+
assert tool.name == PREFERRED_TOOL_NAME
|
|
19
|
+
assert "query" in tool.arg_names
|
|
20
|
+
assert len(tool.arg_names) > 1
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_live_tool_runs_news_search() -> None:
|
|
24
|
+
with WebzioNewsSearchTool() as tool:
|
|
25
|
+
result = tool._run(query="EU AI regulation", k=1)
|
|
26
|
+
assert isinstance(result, str)
|
|
27
|
+
assert len(result) > 0
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from types import SimpleNamespace
|
|
5
|
+
from unittest.mock import MagicMock, patch
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from crewai.tools import BaseTool
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
|
|
11
|
+
from crewai_webzio.consts import (
|
|
12
|
+
DEFAULT_MCP_URL,
|
|
13
|
+
MCP_TRANSPORT,
|
|
14
|
+
PREFERRED_TOOL_NAME,
|
|
15
|
+
TOKEN_ENV_NAME,
|
|
16
|
+
)
|
|
17
|
+
from crewai_webzio.tool import (
|
|
18
|
+
WebzConfigError,
|
|
19
|
+
WebzioNewsSearchTool,
|
|
20
|
+
build_server_params,
|
|
21
|
+
pick_news_search_tool,
|
|
22
|
+
resolve_api_token,
|
|
23
|
+
resolve_mcp_url,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
PACKAGE_ROOT = Path(__file__).resolve().parents[1]
|
|
27
|
+
TOOL_SOURCE = (PACKAGE_ROOT / "crewai_webzio" / "tool.py").read_text(encoding="utf-8")
|
|
28
|
+
CONSTS_SOURCE = (PACKAGE_ROOT / "crewai_webzio" / "consts.py").read_text(encoding="utf-8")
|
|
29
|
+
|
|
30
|
+
FILTER_NAMES_OWNED_BY_MCP = (
|
|
31
|
+
"allow_all_dates",
|
|
32
|
+
"exclude_domain",
|
|
33
|
+
"domain_rank_gte",
|
|
34
|
+
"domain_rank_lte",
|
|
35
|
+
"trust_category",
|
|
36
|
+
"political_bias",
|
|
37
|
+
"min_similarity",
|
|
38
|
+
"allow_multiple_chunks_per_article",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class LiveArgsSchema(BaseModel):
|
|
43
|
+
query: str = Field(..., description="Search query")
|
|
44
|
+
k: int = Field(default=10, description="Number of results")
|
|
45
|
+
extra_filter: str = Field(default="x", description="From MCP server")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class FakeLiveTool(BaseTool):
|
|
49
|
+
name: str = PREFERRED_TOOL_NAME
|
|
50
|
+
description: str = "Live MCP news search tool"
|
|
51
|
+
args_schema: type[BaseModel] = LiveArgsSchema
|
|
52
|
+
|
|
53
|
+
def _run(self, **kwargs: object) -> str:
|
|
54
|
+
return f"result:{kwargs.get('query')}"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class FakeMcpAdapter:
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
serverparams: dict,
|
|
61
|
+
*tool_names: str,
|
|
62
|
+
connect_timeout: int = 30,
|
|
63
|
+
) -> None:
|
|
64
|
+
self.serverparams = serverparams
|
|
65
|
+
self.tool_names = tool_names
|
|
66
|
+
self.connect_timeout = connect_timeout
|
|
67
|
+
self._stopped = False
|
|
68
|
+
self.tools = [FakeLiveTool()]
|
|
69
|
+
|
|
70
|
+
def stop(self) -> None:
|
|
71
|
+
self._stopped = True
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_resolve_api_token_requires_value(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
75
|
+
monkeypatch.delenv(TOKEN_ENV_NAME, raising=False)
|
|
76
|
+
with pytest.raises(WebzConfigError, match="missing Webz API token"):
|
|
77
|
+
resolve_api_token()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_resolve_api_token_prefers_argument(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
81
|
+
monkeypatch.setenv(TOKEN_ENV_NAME, "from-env")
|
|
82
|
+
assert resolve_api_token(" from-arg ") == "from-arg"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_resolve_mcp_url_default_and_override(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
86
|
+
monkeypatch.delenv("WEBZ_MCP_URL", raising=False)
|
|
87
|
+
assert resolve_mcp_url() == DEFAULT_MCP_URL
|
|
88
|
+
assert resolve_mcp_url("https://localhost:8765/mcp/") == "https://localhost:8765/mcp"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_build_server_params_uses_bearer_and_streamable_http() -> None:
|
|
92
|
+
params = build_server_params("secret-token", mcp_url="https://example.test/mcp")
|
|
93
|
+
assert params["url"] == "https://example.test/mcp"
|
|
94
|
+
assert params["transport"] == MCP_TRANSPORT
|
|
95
|
+
assert params["headers"]["Authorization"] == "Bearer secret-token"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_pick_news_search_tool_prefers_named_tool() -> None:
|
|
99
|
+
preferred = SimpleNamespace(name=PREFERRED_TOOL_NAME)
|
|
100
|
+
other = SimpleNamespace(name="other_tool")
|
|
101
|
+
assert pick_news_search_tool([other, preferred]) is preferred # type: ignore[arg-type]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_pick_news_search_tool_falls_back_to_single_tool() -> None:
|
|
105
|
+
only = SimpleNamespace(name="whatever_the_server_exposes")
|
|
106
|
+
assert pick_news_search_tool([only]) is only # type: ignore[arg-type]
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_pick_news_search_tool_errors_when_preferred_missing_among_many() -> None:
|
|
110
|
+
with pytest.raises(WebzConfigError, match="did not expose"):
|
|
111
|
+
pick_news_search_tool(
|
|
112
|
+
[SimpleNamespace(name="alpha"), SimpleNamespace(name="beta")] # type: ignore[list-item]
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@patch("crewai_webzio.tool.MCPServerAdapter", FakeMcpAdapter)
|
|
117
|
+
def test_webzio_news_search_tool_loads_live_schema_and_delegates_run() -> None:
|
|
118
|
+
tool = WebzioNewsSearchTool(api_token="tok", mcp_url="https://example.test/mcp")
|
|
119
|
+
try:
|
|
120
|
+
assert tool.name == PREFERRED_TOOL_NAME
|
|
121
|
+
assert "query" in tool.arg_names
|
|
122
|
+
assert "extra_filter" in tool.arg_names
|
|
123
|
+
assert tool.description == "Live MCP news search tool"
|
|
124
|
+
assert tool._run(query="EU AI", k=3) == "result:EU AI"
|
|
125
|
+
finally:
|
|
126
|
+
tool.stop()
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@patch("crewai_webzio.tool.MCPServerAdapter", FakeMcpAdapter)
|
|
130
|
+
def test_webzio_news_search_tool_context_manager_stops_adapter() -> None:
|
|
131
|
+
with WebzioNewsSearchTool(api_token="tok") as tool:
|
|
132
|
+
assert tool._adapter is not None
|
|
133
|
+
adapter = tool._adapter
|
|
134
|
+
assert adapter._stopped is True
|
|
135
|
+
assert tool._adapter is None
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_wrapper_source_does_not_hardcode_mcp_filters() -> None:
|
|
139
|
+
combined = TOOL_SOURCE + CONSTS_SOURCE
|
|
140
|
+
for name in FILTER_NAMES_OWNED_BY_MCP:
|
|
141
|
+
assert name not in combined, f"wrapper must not hardcode MCP filter {name}"
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_public_exports() -> None:
|
|
145
|
+
import crewai_webzio
|
|
146
|
+
|
|
147
|
+
for name in (
|
|
148
|
+
"WebzioNewsSearchTool",
|
|
149
|
+
"WebzConfigError",
|
|
150
|
+
"DEFAULT_MCP_URL",
|
|
151
|
+
"TOKEN_ENV_NAME",
|
|
152
|
+
"build_server_params",
|
|
153
|
+
"resolve_api_token",
|
|
154
|
+
"resolve_mcp_url",
|
|
155
|
+
):
|
|
156
|
+
assert hasattr(crewai_webzio, name)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Upstream contribution to crewAIInc/crewAI
|
|
2
|
+
|
|
3
|
+
This directory contains files ready to copy into a PR against [crewAIInc/crewAI](https://github.com/crewAIInc/crewAI).
|
|
4
|
+
|
|
5
|
+
## PR checklist
|
|
6
|
+
|
|
7
|
+
1. Copy `lib/crewai-tools/src/crewai_tools/tools/webzio_tools/webzio_news_search_tool.py` into the fork.
|
|
8
|
+
2. Add to `lib/crewai-tools/src/crewai_tools/tools/__init__.py`:
|
|
9
|
+
```python
|
|
10
|
+
from crewai_tools.tools.webzio_tools.webzio_news_search_tool import (
|
|
11
|
+
WebzioNewsSearchTool,
|
|
12
|
+
)
|
|
13
|
+
```
|
|
14
|
+
and add `"WebzioNewsSearchTool"` to `__all__`.
|
|
15
|
+
3. Add to `lib/crewai-tools/src/crewai_tools/__init__.py` the same import and export.
|
|
16
|
+
4. Copy `lib/crewai-tools/tests/tools/webzio_news_search_tool_test.py`.
|
|
17
|
+
5. Copy `docs/en/tools/search-research/webzionewssearchtool.mdx`.
|
|
18
|
+
6. Add a card to `docs/en/tools/search-research/overview.mdx` (Search & Research section).
|
|
19
|
+
7. Register the page in the docs nav (`docs.json` or equivalent Mintlify config).
|
|
20
|
+
8. Regenerate tool specs:
|
|
21
|
+
```bash
|
|
22
|
+
cd lib/crewai-tools
|
|
23
|
+
python -m crewai_tools.generate_tool_specs
|
|
24
|
+
```
|
|
25
|
+
9. Run tests:
|
|
26
|
+
```bash
|
|
27
|
+
pytest lib/crewai-tools/tests/tools/webzio_news_search_tool_test.py
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
No new vendor SDK dependency is required — the tool uses the existing `mcp` extra via `MCPServerAdapter`.
|
|
31
|
+
|
|
32
|
+
## Suggested PR title
|
|
33
|
+
|
|
34
|
+
`feat(tools): add WebzioNewsSearchTool for Webz.io news search via MCP`
|
|
35
|
+
|
|
36
|
+
## Suggested PR description
|
|
37
|
+
|
|
38
|
+
- Adds `WebzioNewsSearchTool` wrapping the hosted Webz News Search MCP server
|
|
39
|
+
- Filter schema loaded live from MCP `tools/list` (not hardcoded)
|
|
40
|
+
- Auth via `WEBZ_API_TOKEN` (Bearer) and optional `WEBZ_MCP_URL`
|
|
41
|
+
- Docs page under Search & Research, modeled on ExaSearchTool
|
|
42
|
+
- Unit tests mock `MCPServerAdapter` (no live token in CI)
|
|
43
|
+
|
|
44
|
+
Maintained by Webz.io. Standalone PyPI package: [crewai-webzio](https://pypi.org/project/crewai-webzio/).
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Webzio News Search Tool
|
|
3
|
+
description: Search global news with Webz.io from CrewAI agents. Semantic ranking, rich filters, and live MCP schemas.
|
|
4
|
+
icon: newspaper
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Webzio News Search Tool
|
|
8
|
+
|
|
9
|
+
> Search global news with [Webz.io](https://webz.io), the contextual news search API. Get token-efficient article excerpts with titles, URLs, and metadata.
|
|
10
|
+
|
|
11
|
+
The `WebzioNewsSearchTool` lets CrewAI agents search news and current events using [Webz.io News Search](https://docs.webz.io/docs/webz/news-search-api-mcp). It returns the most relevant articles for any query, with rich filters loaded live from the hosted MCP server.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
Install the CrewAI tools package with MCP support:
|
|
16
|
+
|
|
17
|
+
```shell
|
|
18
|
+
pip install 'crewai[tools]'
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Environment Variables
|
|
22
|
+
|
|
23
|
+
Set your Webz API token as an environment variable:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
export WEBZ_API_TOKEN='your_webz_api_token'
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
<Note>
|
|
30
|
+
The environment variable name is `WEBZ_API_TOKEN` (not `WEBZ_API_KEY`). Get an API key from the [Webz.io dashboard](https://webz.io).
|
|
31
|
+
</Note>
|
|
32
|
+
|
|
33
|
+
## Example Usage
|
|
34
|
+
|
|
35
|
+
Here's how to use the `WebzioNewsSearchTool` within a CrewAI agent:
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
import os
|
|
39
|
+
from crewai import Agent, Task, Crew
|
|
40
|
+
from crewai_tools import WebzioNewsSearchTool
|
|
41
|
+
|
|
42
|
+
with WebzioNewsSearchTool(api_token=os.environ["WEBZ_API_TOKEN"]) as news_tool:
|
|
43
|
+
researcher = Agent(
|
|
44
|
+
role="Research Analyst",
|
|
45
|
+
goal="Find recent news on any topic",
|
|
46
|
+
backstory="Expert researcher with access to Webz.io news search.",
|
|
47
|
+
tools=[news_tool],
|
|
48
|
+
verbose=True,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
research_task = Task(
|
|
52
|
+
description="Find the top 3 recent breakthroughs in quantum computing.",
|
|
53
|
+
expected_output="A summary of the top 3 breakthroughs with source URLs.",
|
|
54
|
+
agent=researcher,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
crew = Crew(agents=[researcher], tasks=[research_task], verbose=True)
|
|
58
|
+
result = crew.kickoff()
|
|
59
|
+
print(result)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Use the tool as a context manager (or call `stop()` when done) to shut down the MCP session.
|
|
63
|
+
|
|
64
|
+
## Configuration Options
|
|
65
|
+
|
|
66
|
+
The `WebzioNewsSearchTool` accepts the following parameters during initialization:
|
|
67
|
+
|
|
68
|
+
* `api_token` (str, optional): Your Webz API token. Falls back to the `WEBZ_API_TOKEN` environment variable if not provided.
|
|
69
|
+
* `mcp_url` (str, optional): Custom MCP server URL. Falls back to the `WEBZ_MCP_URL` environment variable, then `https://news-search-mcp.webz.io/mcp`.
|
|
70
|
+
* `connect_timeout` (int, optional): Connection timeout in seconds (default: `30`).
|
|
71
|
+
|
|
72
|
+
When calling the tool (or when an agent invokes it), filter parameters are loaded live from MCP `tools/list`. Common parameters include:
|
|
73
|
+
|
|
74
|
+
* `query` (str): **Required**. Natural-language search query.
|
|
75
|
+
* `k` (int, optional): Number of results (1–100, default 10).
|
|
76
|
+
* `days` (int, optional): Lookback window in days.
|
|
77
|
+
* `language` (list[str], optional): e.g. `["english"]`.
|
|
78
|
+
* `country` (list[str], optional): ISO-2 codes, e.g. `["US", "GB"]`.
|
|
79
|
+
* `sentiment` (str, optional): `positive`, `negative`, or `neutral`.
|
|
80
|
+
* `ticker` (list[str], optional): Stock symbols, e.g. `["NVDA"]`.
|
|
81
|
+
* `domain` / `exclude_domain` (list[str], optional): Restrict or exclude publishers.
|
|
82
|
+
|
|
83
|
+
For the full filter reference, see the [Webz MCP tool reference](https://docs.webz.io/docs/webz/news-search-api-mcp#tool-reference).
|
|
84
|
+
|
|
85
|
+
## Advanced Usage
|
|
86
|
+
|
|
87
|
+
For filtered news research, steer the agent in the task description:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
research_task = Task(
|
|
91
|
+
description=(
|
|
92
|
+
"Search Webz news for analyst reaction to Nvidia earnings. "
|
|
93
|
+
"Use ticker NVDA, the last 7 days, English only, and limit to 5 results."
|
|
94
|
+
),
|
|
95
|
+
expected_output="Headline, publisher, and URL for each article.",
|
|
96
|
+
agent=researcher,
|
|
97
|
+
)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Using Webz via MCP
|
|
101
|
+
|
|
102
|
+
You can also connect your agent to the hosted Webz MCP server directly:
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from crewai import Agent
|
|
106
|
+
from crewai.mcp import MCPServerHTTP
|
|
107
|
+
|
|
108
|
+
agent = Agent(
|
|
109
|
+
role="Research Analyst",
|
|
110
|
+
goal="Find and analyze news on the web",
|
|
111
|
+
backstory="Expert researcher with access to Webz news search",
|
|
112
|
+
mcps=[
|
|
113
|
+
MCPServerHTTP(
|
|
114
|
+
url="https://news-search-mcp.webz.io/mcp",
|
|
115
|
+
headers={"Authorization": "Bearer YOUR_WEBZ_API_TOKEN"},
|
|
116
|
+
),
|
|
117
|
+
],
|
|
118
|
+
)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Get your API key from the [Webz.io dashboard](https://webz.io). For more on MCP in CrewAI, see the [MCP overview](/en/mcp/overview).
|
|
122
|
+
|
|
123
|
+
## Features
|
|
124
|
+
|
|
125
|
+
* **Natural-Language Search**: Ask questions in plain language, not keywords
|
|
126
|
+
* **Global Coverage**: Semantic search over Webz.io's worldwide news index
|
|
127
|
+
* **Rich Filters**: Language, country, date, sentiment, domain, ticker, and more
|
|
128
|
+
* **Live Schema**: New filters ship with the MCP server — no package update required
|
|
129
|
+
* **Token-Efficient Excerpts**: Focused article snippets with titles and URLs
|
|
130
|
+
|
|
131
|
+
## Resources
|
|
132
|
+
|
|
133
|
+
* [Webz.io News Search MCP documentation](https://docs.webz.io/docs/webz/news-search-api-mcp)
|
|
134
|
+
* [Webz.io dashboard](https://webz.io)
|
|
135
|
+
* [Standalone PyPI package: crewai-webzio](https://pypi.org/project/crewai-webzio/)
|
|
File without changes
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from types import TracebackType
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from crewai.tools import BaseTool, EnvVar
|
|
8
|
+
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr
|
|
9
|
+
|
|
10
|
+
from crewai_tools.adapters.mcp_adapter import MCPServerAdapter
|
|
11
|
+
|
|
12
|
+
DEFAULT_MCP_URL = "https://news-search-mcp.webz.io/mcp"
|
|
13
|
+
TOKEN_ENV_NAME = "WEBZ_API_TOKEN"
|
|
14
|
+
MCP_URL_ENV_NAME = "WEBZ_MCP_URL"
|
|
15
|
+
PREFERRED_TOOL_NAME = "news_search_by_webz"
|
|
16
|
+
MCP_TRANSPORT = "streamable-http"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class WebzConfigError(ValueError):
|
|
20
|
+
"""Raised when the Webz MCP client cannot be configured or loaded."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def resolve_api_token(api_token: str | None = None) -> str:
|
|
24
|
+
token = (api_token or os.getenv(TOKEN_ENV_NAME) or "").strip()
|
|
25
|
+
if not token:
|
|
26
|
+
raise WebzConfigError(
|
|
27
|
+
f"missing Webz API token. set {TOKEN_ENV_NAME} or pass api_token."
|
|
28
|
+
)
|
|
29
|
+
return token
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def resolve_mcp_url(mcp_url: str | None = None) -> str:
|
|
33
|
+
url = (mcp_url or os.getenv(MCP_URL_ENV_NAME) or DEFAULT_MCP_URL).strip()
|
|
34
|
+
if not url:
|
|
35
|
+
raise WebzConfigError("missing MCP url.")
|
|
36
|
+
return url.rstrip("/")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def build_server_params(
|
|
40
|
+
api_token: str | None = None,
|
|
41
|
+
*,
|
|
42
|
+
mcp_url: str | None = None,
|
|
43
|
+
) -> dict[str, Any]:
|
|
44
|
+
token = resolve_api_token(api_token)
|
|
45
|
+
url = resolve_mcp_url(mcp_url)
|
|
46
|
+
return {
|
|
47
|
+
"url": url,
|
|
48
|
+
"transport": MCP_TRANSPORT,
|
|
49
|
+
"headers": {"Authorization": f"Bearer {token}"},
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def pick_news_search_tool(tools: list[BaseTool]) -> BaseTool:
|
|
54
|
+
if not tools:
|
|
55
|
+
raise WebzConfigError("MCP server returned no tools.")
|
|
56
|
+
for tool in tools:
|
|
57
|
+
if tool.name == PREFERRED_TOOL_NAME:
|
|
58
|
+
return tool
|
|
59
|
+
if len(tools) == 1:
|
|
60
|
+
return tools[0]
|
|
61
|
+
names = ", ".join(tool.name for tool in tools)
|
|
62
|
+
raise WebzConfigError(
|
|
63
|
+
f"MCP server did not expose {PREFERRED_TOOL_NAME}. available tools: {names}"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class WebzioNewsSearchToolSchema(BaseModel):
|
|
68
|
+
"""Minimal static schema; replaced at init with the live MCP schema."""
|
|
69
|
+
|
|
70
|
+
model_config = ConfigDict(extra="allow")
|
|
71
|
+
|
|
72
|
+
query: str = Field(..., description="Natural-language news search query")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class WebzioNewsSearchTool(BaseTool):
|
|
76
|
+
"""Search global news with Webz.io via the hosted News Search MCP server.
|
|
77
|
+
|
|
78
|
+
Filter fields are loaded live from MCP ``tools/list``. New server filters appear
|
|
79
|
+
automatically at runtime without republishing crewai-tools.
|
|
80
|
+
|
|
81
|
+
Use as a context manager or call ``stop()`` when done to shut down the MCP session.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
name: str = PREFERRED_TOOL_NAME
|
|
85
|
+
description: str = (
|
|
86
|
+
"Search global news with Webz.io. Returns article excerpts with titles, "
|
|
87
|
+
"URLs, and metadata. Filter by language, country, date, sentiment, domain, "
|
|
88
|
+
"ticker, and more."
|
|
89
|
+
)
|
|
90
|
+
args_schema: type[BaseModel] = WebzioNewsSearchToolSchema
|
|
91
|
+
package_dependencies: list[str] = Field(default_factory=lambda: ["mcp"])
|
|
92
|
+
env_vars: list[EnvVar] = Field(
|
|
93
|
+
default_factory=lambda: [
|
|
94
|
+
EnvVar(
|
|
95
|
+
name=TOKEN_ENV_NAME,
|
|
96
|
+
description="Webz.io API token from the dashboard",
|
|
97
|
+
required=True,
|
|
98
|
+
),
|
|
99
|
+
EnvVar(
|
|
100
|
+
name=MCP_URL_ENV_NAME,
|
|
101
|
+
description="Override MCP endpoint for testing",
|
|
102
|
+
required=False,
|
|
103
|
+
),
|
|
104
|
+
]
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
_adapter: MCPServerAdapter | None = PrivateAttr(default=None)
|
|
108
|
+
_live_tool: BaseTool | None = PrivateAttr(default=None)
|
|
109
|
+
|
|
110
|
+
def __init__(
|
|
111
|
+
self,
|
|
112
|
+
api_token: str | None = None,
|
|
113
|
+
*,
|
|
114
|
+
mcp_url: str | None = None,
|
|
115
|
+
connect_timeout: int = 30,
|
|
116
|
+
**kwargs: Any,
|
|
117
|
+
) -> None:
|
|
118
|
+
super().__init__(**kwargs)
|
|
119
|
+
self._adapter = MCPServerAdapter(
|
|
120
|
+
build_server_params(api_token, mcp_url=mcp_url),
|
|
121
|
+
PREFERRED_TOOL_NAME,
|
|
122
|
+
connect_timeout=connect_timeout,
|
|
123
|
+
)
|
|
124
|
+
self._live_tool = pick_news_search_tool(list(self._adapter.tools))
|
|
125
|
+
self.args_schema = self._live_tool.args_schema
|
|
126
|
+
self.description = self._live_tool.description
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def arg_names(self) -> list[str]:
|
|
130
|
+
"""Live argument names from the MCP tool schema."""
|
|
131
|
+
return list(self.args_schema.model_fields.keys())
|
|
132
|
+
|
|
133
|
+
def _run(self, **kwargs: Any) -> str:
|
|
134
|
+
if self._live_tool is None:
|
|
135
|
+
raise WebzConfigError("MCP news search tool is not initialized.")
|
|
136
|
+
result = self._live_tool._run(**kwargs)
|
|
137
|
+
return str(result)
|
|
138
|
+
|
|
139
|
+
def stop(self) -> None:
|
|
140
|
+
"""Stop the underlying MCP server connection."""
|
|
141
|
+
if self._adapter is not None:
|
|
142
|
+
self._adapter.stop()
|
|
143
|
+
self._adapter = None
|
|
144
|
+
self._live_tool = None
|
|
145
|
+
|
|
146
|
+
def __enter__(self) -> WebzioNewsSearchTool:
|
|
147
|
+
return self
|
|
148
|
+
|
|
149
|
+
def __exit__(
|
|
150
|
+
self,
|
|
151
|
+
exc_type: type[BaseException] | None,
|
|
152
|
+
exc_val: BaseException | None,
|
|
153
|
+
exc_tb: TracebackType | None,
|
|
154
|
+
) -> None:
|
|
155
|
+
self.stop()
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from types import SimpleNamespace
|
|
4
|
+
from unittest.mock import patch
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
from crewai.tools import BaseTool
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
|
|
10
|
+
from crewai_tools.tools.webzio_tools.webzio_news_search_tool import (
|
|
11
|
+
PREFERRED_TOOL_NAME,
|
|
12
|
+
TOKEN_ENV_NAME,
|
|
13
|
+
WebzConfigError,
|
|
14
|
+
WebzioNewsSearchTool,
|
|
15
|
+
build_server_params,
|
|
16
|
+
pick_news_search_tool,
|
|
17
|
+
resolve_api_token,
|
|
18
|
+
resolve_mcp_url,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class LiveArgsSchema(BaseModel):
|
|
23
|
+
query: str = Field(..., description="Search query")
|
|
24
|
+
k: int = Field(default=10, description="Number of results")
|
|
25
|
+
extra_filter: str = Field(default="x", description="From MCP server")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class FakeLiveTool(BaseTool):
|
|
29
|
+
name: str = PREFERRED_TOOL_NAME
|
|
30
|
+
description: str = "Live MCP news search tool"
|
|
31
|
+
args_schema: type[BaseModel] = LiveArgsSchema
|
|
32
|
+
|
|
33
|
+
def _run(self, **kwargs: object) -> str:
|
|
34
|
+
return f"result:{kwargs.get('query')}"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class FakeMcpAdapter:
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
serverparams: dict,
|
|
41
|
+
*tool_names: str,
|
|
42
|
+
connect_timeout: int = 30,
|
|
43
|
+
) -> None:
|
|
44
|
+
self.serverparams = serverparams
|
|
45
|
+
self.tool_names = tool_names
|
|
46
|
+
self.connect_timeout = connect_timeout
|
|
47
|
+
self._stopped = False
|
|
48
|
+
self.tools = [FakeLiveTool()]
|
|
49
|
+
|
|
50
|
+
def stop(self) -> None:
|
|
51
|
+
self._stopped = True
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_resolve_api_token_requires_value(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
55
|
+
monkeypatch.delenv(TOKEN_ENV_NAME, raising=False)
|
|
56
|
+
with pytest.raises(WebzConfigError, match="missing Webz API token"):
|
|
57
|
+
resolve_api_token()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@patch(
|
|
61
|
+
"crewai_tools.tools.webzio_tools.webzio_news_search_tool.MCPServerAdapter",
|
|
62
|
+
FakeMcpAdapter,
|
|
63
|
+
)
|
|
64
|
+
def test_webzio_news_search_tool_loads_live_schema_and_delegates_run() -> None:
|
|
65
|
+
tool = WebzioNewsSearchTool(api_token="tok", mcp_url="https://example.test/mcp")
|
|
66
|
+
try:
|
|
67
|
+
assert tool.name == PREFERRED_TOOL_NAME
|
|
68
|
+
assert "query" in tool.arg_names
|
|
69
|
+
assert "extra_filter" in tool.arg_names
|
|
70
|
+
assert tool._run(query="EU AI", k=3) == "result:EU AI"
|
|
71
|
+
finally:
|
|
72
|
+
tool.stop()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@patch(
|
|
76
|
+
"crewai_tools.tools.webzio_tools.webzio_news_search_tool.MCPServerAdapter",
|
|
77
|
+
FakeMcpAdapter,
|
|
78
|
+
)
|
|
79
|
+
def test_webzio_news_search_tool_context_manager_stops_adapter() -> None:
|
|
80
|
+
with WebzioNewsSearchTool(api_token="tok") as tool:
|
|
81
|
+
assert tool._adapter is not None
|
|
82
|
+
adapter = tool._adapter
|
|
83
|
+
assert adapter._stopped is True
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_pick_news_search_tool_prefers_named_tool() -> None:
|
|
87
|
+
preferred = SimpleNamespace(name=PREFERRED_TOOL_NAME)
|
|
88
|
+
other = SimpleNamespace(name="other_tool")
|
|
89
|
+
assert pick_news_search_tool([other, preferred]) is preferred # type: ignore[arg-type]
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_build_server_params_uses_bearer_header() -> None:
|
|
93
|
+
params = build_server_params("secret-token", mcp_url="https://example.test/mcp")
|
|
94
|
+
assert params["headers"]["Authorization"] == "Bearer secret-token"
|
|
95
|
+
assert params["transport"] == "streamable-http"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_deprecated_alias_excluded_from_tool_specs() -> None:
|
|
99
|
+
from crewai_tools.generate_tool_specs import ToolSpecExtractor
|
|
100
|
+
|
|
101
|
+
names = {tool["name"] for tool in ToolSpecExtractor().extract_all_tools()}
|
|
102
|
+
assert "WebzioNewsSearchTool" in names
|