webscout-mcp 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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 webscout-mcp contributors
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,184 @@
1
+ Metadata-Version: 2.4
2
+ Name: webscout-mcp
3
+ Version: 0.1.0
4
+ Summary: A smart web search & fetch MCP server with built-in caching, rate-limiting, and content extraction
5
+ Author: webscout-mcp contributors
6
+ License: MIT
7
+ Keywords: mcp,model-context-protocol,web-search,web-scraping,ai-agent,caching
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Topic :: Internet :: WWW/HTTP :: Indexing/Search
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Requires-Python: >=3.10
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: mcp>=1.0.0
21
+ Requires-Dist: httpx>=0.25.0
22
+ Requires-Dist: beautifulsoup4>=4.12.0
23
+ Requires-Dist: lxml>=5.0.0
24
+ Requires-Dist: trafilatura>=1.6.0
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=7.0; extra == "dev"
27
+ Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
28
+ Dynamic: license-file
29
+
30
+ # webscout-mcp
31
+
32
+ Web search and fetch tools for AI agents, as an MCP server. Search, fetch, crawl, and extract structured data from the web — no API keys, no per-request billing, everything stays on your machine.
33
+
34
+ ## Install
35
+
36
+ ```bash
37
+ pip install webscout-mcp
38
+ ```
39
+
40
+ Requires Python 3.10+.
41
+
42
+ ## Quick start
43
+
44
+ Add to your MCP client config (Claude Code, Cursor, Codex, etc.):
45
+
46
+ ```json
47
+ {
48
+ "mcpServers": {
49
+ "webscout": {
50
+ "command": "webscout-mcp",
51
+ "args": []
52
+ }
53
+ }
54
+ }
55
+ ```
56
+
57
+ That's it. Your agent gets six tools:
58
+
59
+ - `web_search` — search via Bing, no key needed
60
+ - `web_fetch` — fetch a page and extract the main article (markdown/text/html)
61
+ - `web_crawl` — bounded BFS crawl with depth and page limits
62
+ - `web_extract` — pull structured data with CSS selectors, attributes, regex
63
+ - `cache_stats` — inspect the local cache
64
+ - `cache_clear` — wipe the cache
65
+
66
+ ## Usage examples
67
+
68
+ ### Search
69
+
70
+ ```json
71
+ web_search(query="best python async libraries", max_results=5)
72
+ ```
73
+
74
+ Returns structured results with title, URL, and snippet.
75
+
76
+ ### Fetch a page
77
+
78
+ ```json
79
+ web_fetch(url="https://example.com", extract=true, output_format="markdown")
80
+ ```
81
+
82
+ `extract=true` runs trafilatura to strip nav, ads, and sidebars — you get clean article content, not raw HTML.
83
+
84
+ ### Extract structured data
85
+
86
+ ```json
87
+ web_extract(
88
+ url="https://example.com/products",
89
+ rules='[
90
+ {"name": "titles", "selector": ".product h2", "multiple": true},
91
+ {"name": "prices", "selector": ".price", "regex": "\\$([\\d.]+)", "multiple": true},
92
+ {"name": "links", "selector": "a.product", "attribute": "href", "multiple": true}
93
+ ]'
94
+ )
95
+ ```
96
+
97
+ Each rule supports `selector`, `attribute`, `multiple`, `regex`, and `default`.
98
+
99
+ ### Crawl a site
100
+
101
+ ```json
102
+ web_crawl(seed_url="https://example.com", max_depth=2, max_pages=10)
103
+ ```
104
+
105
+ Respects same-domain by default. All fetched pages go through the same cache and rate limiter.
106
+
107
+ ## Use as a Python library
108
+
109
+ ```python
110
+ import asyncio
111
+ from webscout_mcp import Config, Fetcher, SearchEngine
112
+
113
+ async def main():
114
+ config = Config.from_env()
115
+ config.ensure_dirs()
116
+
117
+ fetcher = Fetcher(config)
118
+ result = await fetcher.fetch("https://example.com", extract=True)
119
+ print(result.title)
120
+ print(result.content[:500])
121
+ await fetcher.close()
122
+
123
+ search = SearchEngine(config)
124
+ results = await search.search("python async", max_results=5)
125
+ for r in results:
126
+ print(f"{r.position}. {r.title} — {r.url}")
127
+
128
+ asyncio.run(main())
129
+ ```
130
+
131
+ ## How it works
132
+
133
+ - **Search** uses Bing via direct HTTP (no API key). Results are cached by query.
134
+ - **Fetching** uses httpx with exponential-backoff retries, per-domain token-bucket rate limiting, and a 5 MB content cap.
135
+ - **Content extraction** uses trafilatura — the same library behind many read-it-later services.
136
+ - **Caching** is SQLite with TTL and a size cap; old entries are evicted automatically. Repeat fetches and searches cost nothing.
137
+ - **Crawling** is BFS with configurable depth, page count, and same-domain restriction.
138
+
139
+ Everything runs locally. No data leaves your machine.
140
+
141
+ ## Configuration
142
+
143
+ All settings have sensible defaults. Override via environment variables (`WEBSCOUT_` prefix) or CLI flags:
144
+
145
+ | Variable | Default | What it does |
146
+ |---|---|---|
147
+ | `WEBSCOUT_CACHE_DIR` | `~/.cache/webscout` | Where the SQLite cache lives |
148
+ | `WEBSCOUT_CACHE_TTL` | `7200` | Cache entry lifetime in seconds |
149
+ | `WEBSCOUT_CACHE_MAX_SIZE_MB` | `512` | Max cache size before eviction |
150
+ | `WEBSCOUT_REQUEST_TIMEOUT` | `15.0` | HTTP timeout in seconds |
151
+ | `WEBSCOUT_MAX_RETRIES` | `3` | Retry attempts per request |
152
+ | `WEBSCOUT_RATE_LIMIT_PER_SECOND` | `2.0` | Max requests per second per domain |
153
+ | `WEBSCOUT_SEARCH_MAX_RESULTS` | `10` | Default search result count |
154
+ | `WEBSCOUT_CRAWLER_MAX_DEPTH` | `2` | Default crawl depth |
155
+ | `WEBSCOUT_CRAWLER_MAX_PAGES` | `20` | Default max pages per crawl |
156
+
157
+ CLI flags override env vars:
158
+
159
+ ```bash
160
+ webscout-mcp --cache-ttl 3600 --cache-dir /tmp/webscout
161
+ ```
162
+
163
+ ## Transports
164
+
165
+ ```bash
166
+ # stdio (default — works with Claude Code, Cursor, etc.)
167
+ webscout-mcp
168
+
169
+ # SSE (for remote or browser-based clients)
170
+ webscout-mcp --transport sse --host 0.0.0.0 --port 8000
171
+ ```
172
+
173
+ ## Development
174
+
175
+ ```bash
176
+ git clone https://github.com/wxs-lang/webscout-mcp.git
177
+ cd webscout-mcp
178
+ pip install -e ".[dev]"
179
+ pytest
180
+ ```
181
+
182
+ ## License
183
+
184
+ MIT
@@ -0,0 +1,155 @@
1
+ # webscout-mcp
2
+
3
+ Web search and fetch tools for AI agents, as an MCP server. Search, fetch, crawl, and extract structured data from the web — no API keys, no per-request billing, everything stays on your machine.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install webscout-mcp
9
+ ```
10
+
11
+ Requires Python 3.10+.
12
+
13
+ ## Quick start
14
+
15
+ Add to your MCP client config (Claude Code, Cursor, Codex, etc.):
16
+
17
+ ```json
18
+ {
19
+ "mcpServers": {
20
+ "webscout": {
21
+ "command": "webscout-mcp",
22
+ "args": []
23
+ }
24
+ }
25
+ }
26
+ ```
27
+
28
+ That's it. Your agent gets six tools:
29
+
30
+ - `web_search` — search via Bing, no key needed
31
+ - `web_fetch` — fetch a page and extract the main article (markdown/text/html)
32
+ - `web_crawl` — bounded BFS crawl with depth and page limits
33
+ - `web_extract` — pull structured data with CSS selectors, attributes, regex
34
+ - `cache_stats` — inspect the local cache
35
+ - `cache_clear` — wipe the cache
36
+
37
+ ## Usage examples
38
+
39
+ ### Search
40
+
41
+ ```json
42
+ web_search(query="best python async libraries", max_results=5)
43
+ ```
44
+
45
+ Returns structured results with title, URL, and snippet.
46
+
47
+ ### Fetch a page
48
+
49
+ ```json
50
+ web_fetch(url="https://example.com", extract=true, output_format="markdown")
51
+ ```
52
+
53
+ `extract=true` runs trafilatura to strip nav, ads, and sidebars — you get clean article content, not raw HTML.
54
+
55
+ ### Extract structured data
56
+
57
+ ```json
58
+ web_extract(
59
+ url="https://example.com/products",
60
+ rules='[
61
+ {"name": "titles", "selector": ".product h2", "multiple": true},
62
+ {"name": "prices", "selector": ".price", "regex": "\\$([\\d.]+)", "multiple": true},
63
+ {"name": "links", "selector": "a.product", "attribute": "href", "multiple": true}
64
+ ]'
65
+ )
66
+ ```
67
+
68
+ Each rule supports `selector`, `attribute`, `multiple`, `regex`, and `default`.
69
+
70
+ ### Crawl a site
71
+
72
+ ```json
73
+ web_crawl(seed_url="https://example.com", max_depth=2, max_pages=10)
74
+ ```
75
+
76
+ Respects same-domain by default. All fetched pages go through the same cache and rate limiter.
77
+
78
+ ## Use as a Python library
79
+
80
+ ```python
81
+ import asyncio
82
+ from webscout_mcp import Config, Fetcher, SearchEngine
83
+
84
+ async def main():
85
+ config = Config.from_env()
86
+ config.ensure_dirs()
87
+
88
+ fetcher = Fetcher(config)
89
+ result = await fetcher.fetch("https://example.com", extract=True)
90
+ print(result.title)
91
+ print(result.content[:500])
92
+ await fetcher.close()
93
+
94
+ search = SearchEngine(config)
95
+ results = await search.search("python async", max_results=5)
96
+ for r in results:
97
+ print(f"{r.position}. {r.title} — {r.url}")
98
+
99
+ asyncio.run(main())
100
+ ```
101
+
102
+ ## How it works
103
+
104
+ - **Search** uses Bing via direct HTTP (no API key). Results are cached by query.
105
+ - **Fetching** uses httpx with exponential-backoff retries, per-domain token-bucket rate limiting, and a 5 MB content cap.
106
+ - **Content extraction** uses trafilatura — the same library behind many read-it-later services.
107
+ - **Caching** is SQLite with TTL and a size cap; old entries are evicted automatically. Repeat fetches and searches cost nothing.
108
+ - **Crawling** is BFS with configurable depth, page count, and same-domain restriction.
109
+
110
+ Everything runs locally. No data leaves your machine.
111
+
112
+ ## Configuration
113
+
114
+ All settings have sensible defaults. Override via environment variables (`WEBSCOUT_` prefix) or CLI flags:
115
+
116
+ | Variable | Default | What it does |
117
+ |---|---|---|
118
+ | `WEBSCOUT_CACHE_DIR` | `~/.cache/webscout` | Where the SQLite cache lives |
119
+ | `WEBSCOUT_CACHE_TTL` | `7200` | Cache entry lifetime in seconds |
120
+ | `WEBSCOUT_CACHE_MAX_SIZE_MB` | `512` | Max cache size before eviction |
121
+ | `WEBSCOUT_REQUEST_TIMEOUT` | `15.0` | HTTP timeout in seconds |
122
+ | `WEBSCOUT_MAX_RETRIES` | `3` | Retry attempts per request |
123
+ | `WEBSCOUT_RATE_LIMIT_PER_SECOND` | `2.0` | Max requests per second per domain |
124
+ | `WEBSCOUT_SEARCH_MAX_RESULTS` | `10` | Default search result count |
125
+ | `WEBSCOUT_CRAWLER_MAX_DEPTH` | `2` | Default crawl depth |
126
+ | `WEBSCOUT_CRAWLER_MAX_PAGES` | `20` | Default max pages per crawl |
127
+
128
+ CLI flags override env vars:
129
+
130
+ ```bash
131
+ webscout-mcp --cache-ttl 3600 --cache-dir /tmp/webscout
132
+ ```
133
+
134
+ ## Transports
135
+
136
+ ```bash
137
+ # stdio (default — works with Claude Code, Cursor, etc.)
138
+ webscout-mcp
139
+
140
+ # SSE (for remote or browser-based clients)
141
+ webscout-mcp --transport sse --host 0.0.0.0 --port 8000
142
+ ```
143
+
144
+ ## Development
145
+
146
+ ```bash
147
+ git clone https://github.com/wxs-lang/webscout-mcp.git
148
+ cd webscout-mcp
149
+ pip install -e ".[dev]"
150
+ pytest
151
+ ```
152
+
153
+ ## License
154
+
155
+ MIT
@@ -0,0 +1,47 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "webscout-mcp"
7
+ version = "0.1.0"
8
+ description = "A smart web search & fetch MCP server with built-in caching, rate-limiting, and content extraction"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = {text = "MIT"}
12
+ authors = [{name = "webscout-mcp contributors"}]
13
+ keywords = ["mcp", "model-context-protocol", "web-search", "web-scraping", "ai-agent", "caching"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Topic :: Internet :: WWW/HTTP :: Indexing/Search",
23
+ "Topic :: Software Development :: Libraries :: Python Modules",
24
+ ]
25
+ dependencies = [
26
+ "mcp>=1.0.0",
27
+ "httpx>=0.25.0",
28
+ "beautifulsoup4>=4.12.0",
29
+ "lxml>=5.0.0",
30
+ "trafilatura>=1.6.0",
31
+ ]
32
+
33
+ [project.optional-dependencies]
34
+ dev = [
35
+ "pytest>=7.0",
36
+ "pytest-asyncio>=0.21",
37
+ ]
38
+
39
+ [project.scripts]
40
+ webscout-mcp = "webscout_mcp.__main__:main"
41
+
42
+ [tool.setuptools.packages.find]
43
+ include = ["webscout_mcp*"]
44
+
45
+ [tool.pytest.ini_options]
46
+ asyncio_mode = "auto"
47
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,185 @@
1
+ """Basic tests for webscout-mcp core modules.
2
+
3
+ These tests focus on pure-logic components that don't require network access.
4
+ Network-dependent tests are marked and can be run with --run-network.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import os
11
+ import tempfile
12
+ from pathlib import Path
13
+
14
+ import pytest
15
+
16
+ from webscout_mcp.cache import Cache
17
+ from webscout_mcp.config import Config
18
+ from webscout_mcp.extractor import DataExtractor, ExtractionRule
19
+ from webscout_mcp.utils import TokenBucket, is_valid_url, normalize_url, truncate_text
20
+
21
+
22
+ # --- Config ---
23
+
24
+ class TestConfig:
25
+ def test_defaults(self):
26
+ cfg = Config()
27
+ assert cfg.cache_ttl == 7200
28
+ assert cfg.request_timeout == 15.0
29
+ assert cfg.search_max_results == 10
30
+
31
+ def test_from_env(self, monkeypatch):
32
+ monkeypatch.setenv("WEBSCOUT_CACHE_TTL", "3600")
33
+ monkeypatch.setenv("WEBSCOUT_SEARCH_MAX_RESULTS", "5")
34
+ cfg = Config.from_env()
35
+ assert cfg.cache_ttl == 3600
36
+ assert cfg.search_max_results == 5
37
+
38
+ def test_ensure_dirs(self):
39
+ with tempfile.TemporaryDirectory() as tmpdir:
40
+ cfg = Config(cache_dir=Path(tmpdir) / "test_cache")
41
+ cfg.ensure_dirs()
42
+ assert (Path(tmpdir) / "test_cache").exists()
43
+
44
+
45
+ # --- Cache ---
46
+
47
+ class TestCache:
48
+ @pytest.fixture
49
+ def cache(self):
50
+ with tempfile.TemporaryDirectory() as tmpdir:
51
+ c = Cache(Path(tmpdir) / "test.db", ttl=3600, max_size_mb=10)
52
+ yield c
53
+
54
+ def test_set_and_get(self, cache):
55
+ cache.set("https://example.com", "hello world", "text/plain")
56
+ result = cache.get("https://example.com")
57
+ assert result is not None
58
+ assert result["value"] == "hello world"
59
+ assert result["cached"] is True
60
+
61
+ def test_get_missing(self, cache):
62
+ assert cache.get("https://nonexistent.com") is None
63
+
64
+ def test_expiry(self, cache):
65
+ cache.set("https://example.com", "data", ttl=0)
66
+ # ttl=0 means expires immediately
67
+ import time
68
+ time.sleep(0.1)
69
+ assert cache.get("https://example.com") is None
70
+
71
+ def test_delete(self, cache):
72
+ cache.set("https://example.com", "data")
73
+ cache.delete("https://example.com")
74
+ assert cache.get("https://example.com") is None
75
+
76
+ def test_clear(self, cache):
77
+ cache.set("https://a.com", "a")
78
+ cache.set("https://b.com", "b")
79
+ deleted = cache.clear()
80
+ assert deleted == 2
81
+ assert cache.get("https://a.com") is None
82
+
83
+ def test_stats(self, cache):
84
+ cache.set("https://example.com", "hello")
85
+ stats = cache.stats()
86
+ assert stats["entries"] == 1
87
+ assert stats["total_size_bytes"] > 0
88
+
89
+
90
+ # --- Utils ---
91
+
92
+ class TestUtils:
93
+ def test_normalize_url(self):
94
+ assert normalize_url("HTTPS://Example.COM/path//to//page/") == "https://example.com/path/to/page/"
95
+ assert normalize_url("http://example.com:80/path") == "http://example.com/path"
96
+ assert normalize_url("https://example.com:443/path") == "https://example.com/path"
97
+
98
+ def test_normalize_url_strips_fragment(self):
99
+ result = normalize_url("https://example.com/page#section")
100
+ assert "#" not in result
101
+
102
+ def test_is_valid_url(self):
103
+ assert is_valid_url("https://example.com")
104
+ assert is_valid_url("http://example.com/path?q=1")
105
+ assert not is_valid_url("not a url")
106
+ assert not is_valid_url("ftp://example.com")
107
+ assert not is_valid_url("")
108
+
109
+ def test_truncate_text(self):
110
+ short = "hello"
111
+ assert truncate_text(short, 100) == short
112
+
113
+ long_text = "a" * 1000
114
+ result = truncate_text(long_text, 100)
115
+ assert len(result) < 1000
116
+ assert "truncated" in result
117
+
118
+ @pytest.mark.asyncio
119
+ async def test_token_bucket(self):
120
+ bucket = TokenBucket(rate=10.0, burst=2)
121
+ # Should be able to acquire burst tokens quickly
122
+ await bucket.acquire("https://example.com")
123
+ await bucket.acquire("https://example.com")
124
+ # Third should require a short wait
125
+ await bucket.acquire("https://example.com")
126
+
127
+
128
+ # --- Extractor ---
129
+
130
+ class TestExtractor:
131
+ SAMPLE_HTML = """
132
+ <html>
133
+ <head><title>Test Page</title></head>
134
+ <body>
135
+ <h1 class="title">Hello World</h1>
136
+ <p class="price">$29.99</p>
137
+ <a href="https://example.com/1" class="item">Item 1</a>
138
+ <a href="https://example.com/2" class="item">Item 2</a>
139
+ <a href="https://example.com/3" class="item">Item 3</a>
140
+ <div class="description">This is a <b>great</b> product.</div>
141
+ </body>
142
+ </html>
143
+ """
144
+
145
+ def test_extract_single_text(self):
146
+ extractor = DataExtractor(Config(), None) # fetcher not needed for HTML extraction
147
+ rules = [ExtractionRule(name="title", selector="h1.title")]
148
+ result = extractor.extract_from_html(self.SAMPLE_HTML, rules)
149
+ assert result["title"] == "Hello World"
150
+
151
+ def test_extract_with_regex(self):
152
+ extractor = DataExtractor(Config(), None)
153
+ rules = [ExtractionRule(name="price", selector=".price", regex=r"\$([\d.]+)")]
154
+ result = extractor.extract_from_html(self.SAMPLE_HTML, rules)
155
+ assert result["price"] == "29.99"
156
+
157
+ def test_extract_attribute_multiple(self):
158
+ extractor = DataExtractor(Config(), None)
159
+ rules = [ExtractionRule(name="links", selector="a.item", attribute="href", multiple=True)]
160
+ result = extractor.extract_from_html(self.SAMPLE_HTML, rules)
161
+ assert result["links"] == [
162
+ "https://example.com/1",
163
+ "https://example.com/2",
164
+ "https://example.com/3",
165
+ ]
166
+
167
+ def test_extract_default_on_missing(self):
168
+ extractor = DataExtractor(Config(), None)
169
+ rules = [ExtractionRule(name="missing", selector=".nonexistent", default="N/A")]
170
+ result = extractor.extract_from_html(self.SAMPLE_HTML, rules)
171
+ assert result["missing"] == "N/A"
172
+
173
+
174
+ # --- MCP Server (import test) ---
175
+
176
+ class TestServer:
177
+ def test_create_server(self):
178
+ """Verify the server can be created without errors."""
179
+ with tempfile.TemporaryDirectory() as tmpdir:
180
+ cfg = Config(cache_dir=Path(tmpdir))
181
+ from webscout_mcp.server import create_server
182
+ mcp = create_server(cfg)
183
+ assert mcp is not None
184
+ # FastMCP stores tools internally
185
+ assert hasattr(mcp, "tool")
@@ -0,0 +1,11 @@
1
+ """webscout-mcp: A smart web search & fetch MCP server."""
2
+
3
+ __version__ = "0.1.0"
4
+ __all__ = ["__version__", "create_server", "Config", "Fetcher", "SearchEngine", "Crawler", "DataExtractor"]
5
+
6
+ from .config import Config
7
+ from .crawler import Crawler
8
+ from .extractor import DataExtractor
9
+ from .fetcher import Fetcher
10
+ from .search import SearchEngine
11
+ from .server import create_server
@@ -0,0 +1,79 @@
1
+ """Command-line entry point for webscout-mcp.
2
+
3
+ Usage:
4
+ webscout-mcp # Start MCP server over stdio (default)
5
+ webscout-mcp --transport sse # Start MCP server over SSE
6
+ webscout-mcp --version # Print version
7
+ webscout-mcp --help # Show help
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import sys
14
+
15
+ from . import __version__
16
+ from .config import Config
17
+
18
+
19
+ def main() -> None:
20
+ parser = argparse.ArgumentParser(
21
+ prog="webscout-mcp",
22
+ description="A smart web search & fetch MCP server with caching and content extraction.",
23
+ )
24
+ parser.add_argument(
25
+ "--version",
26
+ action="version",
27
+ version=f"webscout-mcp {__version__}",
28
+ )
29
+ parser.add_argument(
30
+ "--transport",
31
+ choices=["stdio", "sse"],
32
+ default="stdio",
33
+ help="MCP transport protocol (default: stdio)",
34
+ )
35
+ parser.add_argument(
36
+ "--host",
37
+ default="127.0.0.1",
38
+ help="Host for SSE transport (default: 127.0.0.1)",
39
+ )
40
+ parser.add_argument(
41
+ "--port",
42
+ type=int,
43
+ default=8000,
44
+ help="Port for SSE transport (default: 8000)",
45
+ )
46
+ parser.add_argument(
47
+ "--cache-dir",
48
+ default=None,
49
+ help="Override cache directory",
50
+ )
51
+ parser.add_argument(
52
+ "--cache-ttl",
53
+ type=int,
54
+ default=None,
55
+ help="Override cache TTL in seconds",
56
+ )
57
+
58
+ args = parser.parse_args()
59
+
60
+ # Build config
61
+ config = Config.from_env()
62
+ if args.cache_dir:
63
+ from pathlib import Path
64
+ config.cache_dir = Path(args.cache_dir)
65
+ if args.cache_ttl is not None:
66
+ config.cache_ttl = args.cache_ttl
67
+
68
+ # Create and run server
69
+ from .server import create_server
70
+ mcp = create_server(config)
71
+
72
+ if args.transport == "sse":
73
+ mcp.run(transport="sse", host=args.host, port=args.port)
74
+ else:
75
+ mcp.run(transport="stdio")
76
+
77
+
78
+ if __name__ == "__main__":
79
+ main()