oxylabs-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.
- oxylabs_mcp-0.1.0/.gitignore +11 -0
- oxylabs_mcp-0.1.0/Dockerfile +24 -0
- oxylabs_mcp-0.1.0/LICENSE +21 -0
- oxylabs_mcp-0.1.0/Makefile +34 -0
- oxylabs_mcp-0.1.0/PKG-INFO +148 -0
- oxylabs_mcp-0.1.0/README.md +125 -0
- oxylabs_mcp-0.1.0/pyproject.toml +53 -0
- oxylabs_mcp-0.1.0/smithery.yaml +21 -0
- oxylabs_mcp-0.1.0/src/oxylabs_mcp/__init__.py +10 -0
- oxylabs_mcp-0.1.0/src/oxylabs_mcp/server.py +132 -0
- oxylabs_mcp-0.1.0/src/oxylabs_mcp/utils.py +96 -0
- oxylabs_mcp-0.1.0/tests/__init__.py +0 -0
- oxylabs_mcp-0.1.0/tests/fixtures/__init__.py +0 -0
- oxylabs_mcp-0.1.0/tests/fixtures/after_strip.html +2 -0
- oxylabs_mcp-0.1.0/tests/fixtures/before_strip.html +19 -0
- oxylabs_mcp-0.1.0/tests/integration/__init__.py +0 -0
- oxylabs_mcp-0.1.0/tests/integration/test_server.py +279 -0
- oxylabs_mcp-0.1.0/tests/unit/__init__.py +0 -0
- oxylabs_mcp-0.1.0/tests/unit/test_utils.py +58 -0
- oxylabs_mcp-0.1.0/uv.lock +592 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim
|
|
2
|
+
|
|
3
|
+
ENV UV_COMPILE_BYTECODE=1
|
|
4
|
+
ENV UV_LINK_MODE=copy
|
|
5
|
+
ENV UV_CACHE_DIR=/opt/uv-cache/
|
|
6
|
+
|
|
7
|
+
RUN apt-get update && apt-get install -y --no-install-recommends git
|
|
8
|
+
|
|
9
|
+
WORKDIR /app
|
|
10
|
+
|
|
11
|
+
RUN --mount=type=cache,target=UV_CACHE_DIR \
|
|
12
|
+
--mount=type=bind,source=uv.lock,target=uv.lock \
|
|
13
|
+
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
|
14
|
+
uv sync --frozen --no-install-project --no-dev --no-editable
|
|
15
|
+
|
|
16
|
+
ADD . /app
|
|
17
|
+
|
|
18
|
+
RUN --mount=type=cache,target=UV_CACHE_DIR \
|
|
19
|
+
uv sync --frozen --no-dev --no-editable
|
|
20
|
+
|
|
21
|
+
# Add virtual environment to PATH
|
|
22
|
+
ENV PATH="/app/.venv/bin:$PATH"
|
|
23
|
+
|
|
24
|
+
ENTRYPOINT ["oxylabs-mcp"]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Oxylabs
|
|
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,34 @@
|
|
|
1
|
+
src_dir := src/
|
|
2
|
+
test_dir := tests/
|
|
3
|
+
current_dir = $(shell pwd)
|
|
4
|
+
|
|
5
|
+
python ?= python3
|
|
6
|
+
virtualenv_dir ?= .venv
|
|
7
|
+
|
|
8
|
+
.PHONY: install_deps
|
|
9
|
+
install_deps: $(virtualenv_dir)
|
|
10
|
+
uv sync
|
|
11
|
+
|
|
12
|
+
.PHONY: lint
|
|
13
|
+
lint: install_deps
|
|
14
|
+
uv run mypy $(src_dir)
|
|
15
|
+
uv run ruff check
|
|
16
|
+
|
|
17
|
+
.PHONY: format
|
|
18
|
+
format: $(virtualenv_dir)
|
|
19
|
+
uv run ruff check --fix .
|
|
20
|
+
|
|
21
|
+
.PHONY: test
|
|
22
|
+
test: install_deps
|
|
23
|
+
uv run pytest ./tests
|
|
24
|
+
|
|
25
|
+
.PHONY: run
|
|
26
|
+
run: install_deps
|
|
27
|
+
npx @modelcontextprotocol/inspector \
|
|
28
|
+
uv \
|
|
29
|
+
--directory $(current_dir) \
|
|
30
|
+
run \
|
|
31
|
+
oxylabs-mcp
|
|
32
|
+
|
|
33
|
+
$(virtualenv_dir):
|
|
34
|
+
$(python) -m venv $@ --symlinks
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: oxylabs-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Oxylabs MCP server
|
|
5
|
+
Project-URL: Homepage, https://github.com/oxylabs/oxylabs-mcp
|
|
6
|
+
Project-URL: Repository, https://github.com/oxylabs/oxylabs-mcp
|
|
7
|
+
Author-email: Augis Braziunas <augis.braziunas@oxylabs.io>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Requires-Python: >=3.12
|
|
16
|
+
Requires-Dist: httpx>=0.28.1
|
|
17
|
+
Requires-Dist: lxml-html-clean>=0.4.1
|
|
18
|
+
Requires-Dist: lxml>=5.3.0
|
|
19
|
+
Requires-Dist: markdownify>=0.14.1
|
|
20
|
+
Requires-Dist: mcp[cli]>=1.2.0
|
|
21
|
+
Requires-Dist: pydantic>=2.10.5
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# MCP Server for Oxylabs Scraper
|
|
25
|
+
[](https://smithery.ai/server/@oxylabs/oxylabs-mcp)
|
|
26
|
+
|
|
27
|
+
The scraper tool is an asynchronous utility that leverages the Oxylabs Web Scraper API to fetch and process content from a given URL. This tool is designed to handle various scraping scenarios efficiently, with flexible options for parsing and rendering web pages.
|
|
28
|
+
|
|
29
|
+
## Key Features
|
|
30
|
+
|
|
31
|
+
1. **URL Scraping**
|
|
32
|
+
- Fetches the content of a specified URL using Oxylabs' API.
|
|
33
|
+
|
|
34
|
+
2. **HTML Parsing**
|
|
35
|
+
- If parsing is enabled, the content is returned in a processed format, suitable for handling by downstream systems like language models (LLMs).
|
|
36
|
+
- If parsing is disabled, the tool strips unnecessary HTML tags and converts the cleaned content into a Markdown file for easier readability and compatibility.
|
|
37
|
+
|
|
38
|
+
3. **JavaScript Rendering**
|
|
39
|
+
- Offers an option to render pages using a headless browser, which ensures dynamic content (e.g., JavaScript-rendered pages) is included in the output.
|
|
40
|
+
- Supports the html rendering option or skipping rendering entirely.
|
|
41
|
+
|
|
42
|
+
## Examples on how to query Claude or other LLM
|
|
43
|
+
- Could you scrape https://oxylabs.io page?
|
|
44
|
+
- Scrape https://www.amazon.de/-/en/Smartphone-Contract-Function-Manufacturer-Exclusive/dp/B0CNKD651V with parse enabled.
|
|
45
|
+
- Scrape https://www.amazon.de/-/en/gp/bestsellers/beauty/ref=zg_bs_nav_beauty_0 with parse and render enabled.
|
|
46
|
+
- Use web unblocker with render to scrape https://oxylabs.io/
|
|
47
|
+
|
|
48
|
+
## Prerequisites
|
|
49
|
+
### Install uv first.
|
|
50
|
+
https://docs.astral.sh/uv/getting-started/installation/#installation-methods
|
|
51
|
+
|
|
52
|
+
## Setup Intructions
|
|
53
|
+
|
|
54
|
+
### Installing via Smithery
|
|
55
|
+
|
|
56
|
+
To install Oxylabs MCP server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@oxylabs/oxylabs-mcp):
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npx -y @smithery/cli install @oxylabs/oxylabs-mcp --client claude
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
### Setup with Claude Desktop
|
|
64
|
+
```json
|
|
65
|
+
# claude_desktop_config.json
|
|
66
|
+
# Can find location through:
|
|
67
|
+
# Claude -> Settings -> Developer -> Edit Config
|
|
68
|
+
{
|
|
69
|
+
"mcpServers": {
|
|
70
|
+
"oxylabs_scraper": {
|
|
71
|
+
"command": "uvx",
|
|
72
|
+
"args": ["oxylabs-mcp"],
|
|
73
|
+
"env": {
|
|
74
|
+
"OXYLABS_USERNAME": "YOUR_USERNAME_HERE",
|
|
75
|
+
"OXYLABS_PASSWORD": "YOUR_PASSWORD_HERE",
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Local/Dev Setup Instructions
|
|
83
|
+
### Clone repo
|
|
84
|
+
`git clone <git:url>`
|
|
85
|
+
### Install dependencies
|
|
86
|
+
Install MCP server dependencies:
|
|
87
|
+
```bash
|
|
88
|
+
cd mcp-server-oxylabs
|
|
89
|
+
|
|
90
|
+
# Create virtual environment and activate it
|
|
91
|
+
uv venv
|
|
92
|
+
|
|
93
|
+
source .venv/bin/activate # MacOS/Linux
|
|
94
|
+
# OR
|
|
95
|
+
.venv/Scripts/activate # Windows
|
|
96
|
+
|
|
97
|
+
# Install dependencies
|
|
98
|
+
uv sync
|
|
99
|
+
```
|
|
100
|
+
### Setup with Claude Desktop
|
|
101
|
+
```json
|
|
102
|
+
# claude_desktop_config.json
|
|
103
|
+
# Can find location through:
|
|
104
|
+
# Claude -> Settings -> Developer -> Edit Config
|
|
105
|
+
{
|
|
106
|
+
"mcpServers": {
|
|
107
|
+
"oxylabs_scraper": {
|
|
108
|
+
"command": "uv",
|
|
109
|
+
"args": [
|
|
110
|
+
"--directory",
|
|
111
|
+
"/<Absolute-path-to-folder>/oxylabs-mcp",
|
|
112
|
+
"run",
|
|
113
|
+
"oxylabs-mcp"
|
|
114
|
+
],
|
|
115
|
+
"env": {
|
|
116
|
+
"OXYLABS_USERNAME": "YOUR_USERNAME_HERE",
|
|
117
|
+
"OXYLABS_PASSWORD": "YOUR_PASSWORD_HERE"
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Debugging
|
|
125
|
+
Run:
|
|
126
|
+
```bash
|
|
127
|
+
make run
|
|
128
|
+
```
|
|
129
|
+
Then access MCP Inspector at `http://localhost:5173`. You may need to add your username and password in the environment variables in the inspector under `OXYLABS_USERNAME` and `OXYLABS_PASSWORD`.
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
**NOTE**
|
|
133
|
+
|
|
134
|
+
Usage with the MCP inspector is affected by an ongoing issue with the Python SDK for MCP, see: https://github.com/modelcontextprotocol/python-sdk/pull/85. For Claude, a forked version of the SDK is used as a temporary fix.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## License
|
|
139
|
+
|
|
140
|
+
This project is licensed under the [MIT License](LICENSE).
|
|
141
|
+
|
|
142
|
+
## About Oxylabs
|
|
143
|
+
|
|
144
|
+
Established in 2015, Oxylabs are a market-leading web intelligence collection
|
|
145
|
+
platform, driven by the highest business, ethics, and compliance standards,
|
|
146
|
+
enabling companies worldwide to unlock data-driven insights.
|
|
147
|
+
|
|
148
|
+
[](https://oxylabs.io/)
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# MCP Server for Oxylabs Scraper
|
|
2
|
+
[](https://smithery.ai/server/@oxylabs/oxylabs-mcp)
|
|
3
|
+
|
|
4
|
+
The scraper tool is an asynchronous utility that leverages the Oxylabs Web Scraper API to fetch and process content from a given URL. This tool is designed to handle various scraping scenarios efficiently, with flexible options for parsing and rendering web pages.
|
|
5
|
+
|
|
6
|
+
## Key Features
|
|
7
|
+
|
|
8
|
+
1. **URL Scraping**
|
|
9
|
+
- Fetches the content of a specified URL using Oxylabs' API.
|
|
10
|
+
|
|
11
|
+
2. **HTML Parsing**
|
|
12
|
+
- If parsing is enabled, the content is returned in a processed format, suitable for handling by downstream systems like language models (LLMs).
|
|
13
|
+
- If parsing is disabled, the tool strips unnecessary HTML tags and converts the cleaned content into a Markdown file for easier readability and compatibility.
|
|
14
|
+
|
|
15
|
+
3. **JavaScript Rendering**
|
|
16
|
+
- Offers an option to render pages using a headless browser, which ensures dynamic content (e.g., JavaScript-rendered pages) is included in the output.
|
|
17
|
+
- Supports the html rendering option or skipping rendering entirely.
|
|
18
|
+
|
|
19
|
+
## Examples on how to query Claude or other LLM
|
|
20
|
+
- Could you scrape https://oxylabs.io page?
|
|
21
|
+
- Scrape https://www.amazon.de/-/en/Smartphone-Contract-Function-Manufacturer-Exclusive/dp/B0CNKD651V with parse enabled.
|
|
22
|
+
- Scrape https://www.amazon.de/-/en/gp/bestsellers/beauty/ref=zg_bs_nav_beauty_0 with parse and render enabled.
|
|
23
|
+
- Use web unblocker with render to scrape https://oxylabs.io/
|
|
24
|
+
|
|
25
|
+
## Prerequisites
|
|
26
|
+
### Install uv first.
|
|
27
|
+
https://docs.astral.sh/uv/getting-started/installation/#installation-methods
|
|
28
|
+
|
|
29
|
+
## Setup Intructions
|
|
30
|
+
|
|
31
|
+
### Installing via Smithery
|
|
32
|
+
|
|
33
|
+
To install Oxylabs MCP server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@oxylabs/oxylabs-mcp):
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npx -y @smithery/cli install @oxylabs/oxylabs-mcp --client claude
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
### Setup with Claude Desktop
|
|
41
|
+
```json
|
|
42
|
+
# claude_desktop_config.json
|
|
43
|
+
# Can find location through:
|
|
44
|
+
# Claude -> Settings -> Developer -> Edit Config
|
|
45
|
+
{
|
|
46
|
+
"mcpServers": {
|
|
47
|
+
"oxylabs_scraper": {
|
|
48
|
+
"command": "uvx",
|
|
49
|
+
"args": ["oxylabs-mcp"],
|
|
50
|
+
"env": {
|
|
51
|
+
"OXYLABS_USERNAME": "YOUR_USERNAME_HERE",
|
|
52
|
+
"OXYLABS_PASSWORD": "YOUR_PASSWORD_HERE",
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Local/Dev Setup Instructions
|
|
60
|
+
### Clone repo
|
|
61
|
+
`git clone <git:url>`
|
|
62
|
+
### Install dependencies
|
|
63
|
+
Install MCP server dependencies:
|
|
64
|
+
```bash
|
|
65
|
+
cd mcp-server-oxylabs
|
|
66
|
+
|
|
67
|
+
# Create virtual environment and activate it
|
|
68
|
+
uv venv
|
|
69
|
+
|
|
70
|
+
source .venv/bin/activate # MacOS/Linux
|
|
71
|
+
# OR
|
|
72
|
+
.venv/Scripts/activate # Windows
|
|
73
|
+
|
|
74
|
+
# Install dependencies
|
|
75
|
+
uv sync
|
|
76
|
+
```
|
|
77
|
+
### Setup with Claude Desktop
|
|
78
|
+
```json
|
|
79
|
+
# claude_desktop_config.json
|
|
80
|
+
# Can find location through:
|
|
81
|
+
# Claude -> Settings -> Developer -> Edit Config
|
|
82
|
+
{
|
|
83
|
+
"mcpServers": {
|
|
84
|
+
"oxylabs_scraper": {
|
|
85
|
+
"command": "uv",
|
|
86
|
+
"args": [
|
|
87
|
+
"--directory",
|
|
88
|
+
"/<Absolute-path-to-folder>/oxylabs-mcp",
|
|
89
|
+
"run",
|
|
90
|
+
"oxylabs-mcp"
|
|
91
|
+
],
|
|
92
|
+
"env": {
|
|
93
|
+
"OXYLABS_USERNAME": "YOUR_USERNAME_HERE",
|
|
94
|
+
"OXYLABS_PASSWORD": "YOUR_PASSWORD_HERE"
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Debugging
|
|
102
|
+
Run:
|
|
103
|
+
```bash
|
|
104
|
+
make run
|
|
105
|
+
```
|
|
106
|
+
Then access MCP Inspector at `http://localhost:5173`. You may need to add your username and password in the environment variables in the inspector under `OXYLABS_USERNAME` and `OXYLABS_PASSWORD`.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
**NOTE**
|
|
110
|
+
|
|
111
|
+
Usage with the MCP inspector is affected by an ongoing issue with the Python SDK for MCP, see: https://github.com/modelcontextprotocol/python-sdk/pull/85. For Claude, a forked version of the SDK is used as a temporary fix.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
This project is licensed under the [MIT License](LICENSE).
|
|
118
|
+
|
|
119
|
+
## About Oxylabs
|
|
120
|
+
|
|
121
|
+
Established in 2015, Oxylabs are a market-leading web intelligence collection
|
|
122
|
+
platform, driven by the highest business, ethics, and compliance standards,
|
|
123
|
+
enabling companies worldwide to unlock data-driven insights.
|
|
124
|
+
|
|
125
|
+
[](https://oxylabs.io/)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "oxylabs-mcp"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Oxylabs MCP server"
|
|
5
|
+
authors = [
|
|
6
|
+
{name="Augis Braziunas", email="augis.braziunas@oxylabs.io"},
|
|
7
|
+
]
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
requires-python = ">=3.12"
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Programming Language :: Python :: 3",
|
|
12
|
+
"Programming Language :: Python :: 3.12",
|
|
13
|
+
"Programming Language :: Python :: 3.13",
|
|
14
|
+
"Development Status :: 4 - Beta",
|
|
15
|
+
"Operating System :: OS Independent",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
license = "MIT"
|
|
19
|
+
license-files = ["LICEN[CS]E*"]
|
|
20
|
+
|
|
21
|
+
dependencies = [
|
|
22
|
+
"httpx>=0.28.1",
|
|
23
|
+
"lxml>=5.3.0",
|
|
24
|
+
"lxml-html-clean>=0.4.1",
|
|
25
|
+
"markdownify>=0.14.1",
|
|
26
|
+
"mcp[cli]>=1.2.0",
|
|
27
|
+
"pydantic>=2.10.5",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[dependency-groups]
|
|
31
|
+
dev = [
|
|
32
|
+
"mypy>=1.14.1",
|
|
33
|
+
"pytest>=8.3.4",
|
|
34
|
+
"pytest-asyncio>=0.25.2",
|
|
35
|
+
"pytest-mock>=3.14.0",
|
|
36
|
+
"ruff>=0.9.1",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[build-system]
|
|
40
|
+
requires = ["hatchling"]
|
|
41
|
+
build-backend = "hatchling.build"
|
|
42
|
+
|
|
43
|
+
# Fork with minimal fix for Claude disconnection errors
|
|
44
|
+
# When https://github.com/modelcontextprotocol/python-sdk/pull/85 merged, change back to official lib.
|
|
45
|
+
[tool.uv.sources]
|
|
46
|
+
mcp = { git = "https://github.com/txbm/python-sdk", rev = "19185626d57fdd670219784d10859c7d0a302c2e" }
|
|
47
|
+
|
|
48
|
+
[project.scripts]
|
|
49
|
+
oxylabs-mcp = "oxylabs_mcp:main"
|
|
50
|
+
|
|
51
|
+
[project.urls]
|
|
52
|
+
Homepage = "https://github.com/oxylabs/oxylabs-mcp"
|
|
53
|
+
Repository = "https://github.com/oxylabs/oxylabs-mcp"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Smithery configuration file: https://smithery.ai/docs/deployments
|
|
2
|
+
|
|
3
|
+
startCommand:
|
|
4
|
+
type: stdio
|
|
5
|
+
configSchema:
|
|
6
|
+
# JSON Schema defining the configuration options for the MCP.
|
|
7
|
+
type: object
|
|
8
|
+
required:
|
|
9
|
+
- oxylabsUsername
|
|
10
|
+
- oxylabsPassword
|
|
11
|
+
properties:
|
|
12
|
+
oxylabsUsername:
|
|
13
|
+
type: string
|
|
14
|
+
description: The username for Oxylabs API authentication.
|
|
15
|
+
oxylabsPassword:
|
|
16
|
+
type: string
|
|
17
|
+
description: The password for Oxylabs API authentication.
|
|
18
|
+
commandFunction:
|
|
19
|
+
# A function that produces the CLI command to start the MCP on stdio.
|
|
20
|
+
|-
|
|
21
|
+
config => ({ command: 'oxylabs-mcp', env: { OXYLABS_USERNAME: config.oxylabsUsername, OXYLABS_PASSWORD: config.oxylabsPassword } })
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
from typing import Annotated, Any, Literal
|
|
2
|
+
from httpx import AsyncClient, BasicAuth, Timeout, HTTPStatusError, RequestError
|
|
3
|
+
from mcp.server.fastmcp import FastMCP
|
|
4
|
+
from dotenv import load_dotenv
|
|
5
|
+
from pydantic import Field
|
|
6
|
+
|
|
7
|
+
from oxylabs_mcp.utils import convert_html_to_md, get_auth_from_env, strip_html
|
|
8
|
+
|
|
9
|
+
OXYLABS_SCRAPER_URL = "https://realtime.oxylabs.io/v1/queries"
|
|
10
|
+
REQUEST_TIMEOUT = 100
|
|
11
|
+
|
|
12
|
+
mcp = FastMCP("oxylabs_mcp", dependencies=["mcp", "httpx"])
|
|
13
|
+
load_dotenv()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@mcp.tool(name="oxylabs_scraper", description="Scrape url using Oxylabs Web Api")
|
|
17
|
+
async def scrape_url(
|
|
18
|
+
url: Annotated[str, Field(description="Url to scrape")],
|
|
19
|
+
parse: Annotated[
|
|
20
|
+
bool | None,
|
|
21
|
+
Field(
|
|
22
|
+
description="Should result be parsed. "
|
|
23
|
+
"If result should not be parsed then html "
|
|
24
|
+
"will be stripped and converted to markdown file"
|
|
25
|
+
)
|
|
26
|
+
] = None,
|
|
27
|
+
render: Annotated[
|
|
28
|
+
Literal["html", "None"] | None,
|
|
29
|
+
Field(
|
|
30
|
+
description="Whether a headless browser should be used "
|
|
31
|
+
"to render the page. See: "
|
|
32
|
+
"https://developers.oxylabs.io/scraper-apis/web-scraper-api/features/javascript-rendering "
|
|
33
|
+
"`html` will return rendered html page "
|
|
34
|
+
"`None` will not use render for scraping."
|
|
35
|
+
)
|
|
36
|
+
] = None
|
|
37
|
+
) -> str:
|
|
38
|
+
"""Scrape Url using Oxylabs scraper api"""
|
|
39
|
+
username, password = get_auth_from_env()
|
|
40
|
+
|
|
41
|
+
async with AsyncClient(
|
|
42
|
+
auth=BasicAuth(username=username, password=password),
|
|
43
|
+
timeout=Timeout(REQUEST_TIMEOUT)
|
|
44
|
+
) as client:
|
|
45
|
+
try:
|
|
46
|
+
json: dict[str, Any] = {"url": url}
|
|
47
|
+
if parse:
|
|
48
|
+
json["parse"] = bool(parse)
|
|
49
|
+
if render and render != "None":
|
|
50
|
+
json["render"] = render
|
|
51
|
+
|
|
52
|
+
response = await client.post(
|
|
53
|
+
OXYLABS_SCRAPER_URL,
|
|
54
|
+
json=json,
|
|
55
|
+
)
|
|
56
|
+
response.raise_for_status()
|
|
57
|
+
|
|
58
|
+
if not bool(parse):
|
|
59
|
+
striped_html = strip_html(
|
|
60
|
+
str(response.json()["results"][0]["content"])
|
|
61
|
+
)
|
|
62
|
+
return convert_html_to_md(striped_html)
|
|
63
|
+
return str(response.json()["results"][0]["content"])
|
|
64
|
+
except HTTPStatusError as e:
|
|
65
|
+
return (
|
|
66
|
+
"HTTP error during POST request: "
|
|
67
|
+
f"{e.response.status_code} - {e.response.text}"
|
|
68
|
+
)
|
|
69
|
+
except RequestError as e:
|
|
70
|
+
return f"Request error during POST request: {e}"
|
|
71
|
+
except Exception as e:
|
|
72
|
+
return f"Error: {str(e) or repr(e)}"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@mcp.tool(
|
|
76
|
+
name="oxylabs_web_unblocker",
|
|
77
|
+
description="Scrape url using Oxylabs Web Unblocker",
|
|
78
|
+
)
|
|
79
|
+
async def scrape_with_web_unblocker(
|
|
80
|
+
url: Annotated[str, Field(description="Url to scrape with web unblocker")],
|
|
81
|
+
render: Annotated[
|
|
82
|
+
Literal["html", "None"] | None,
|
|
83
|
+
Field(
|
|
84
|
+
description="Whether a headless browser should be used "
|
|
85
|
+
"to render the page. See: "
|
|
86
|
+
"https://developers.oxylabs.io/advanced-proxy-solutions/web-unblocker/headless-browser/javascript-rendering "
|
|
87
|
+
"`html` will return rendered html page "
|
|
88
|
+
"`None` will not use render for scraping."
|
|
89
|
+
)
|
|
90
|
+
] = None
|
|
91
|
+
) -> str:
|
|
92
|
+
"""Web Unblocker is an AI-powered proxy solution.
|
|
93
|
+
|
|
94
|
+
This tool manages the unblocking process to extract public data
|
|
95
|
+
even from the most difficult websites.
|
|
96
|
+
"""
|
|
97
|
+
username, password = get_auth_from_env()
|
|
98
|
+
|
|
99
|
+
proxy = f"http://{username}:{password}@unblock.oxylabs.io:60000"
|
|
100
|
+
|
|
101
|
+
headers: dict[str, Any] = {}
|
|
102
|
+
if render and render != "None":
|
|
103
|
+
headers["X-Oxylabs-Render"] = render
|
|
104
|
+
|
|
105
|
+
async with AsyncClient(
|
|
106
|
+
timeout=Timeout(REQUEST_TIMEOUT),
|
|
107
|
+
verify=False,
|
|
108
|
+
proxy=proxy,
|
|
109
|
+
headers=headers
|
|
110
|
+
) as client:
|
|
111
|
+
try:
|
|
112
|
+
response = await client.get(url)
|
|
113
|
+
response.raise_for_status()
|
|
114
|
+
striped_html = strip_html(response.text)
|
|
115
|
+
return convert_html_to_md(striped_html)
|
|
116
|
+
except HTTPStatusError as e:
|
|
117
|
+
return (
|
|
118
|
+
"HTTP error during POST request: "
|
|
119
|
+
f"{e.response.status_code} - {e.response.text}"
|
|
120
|
+
)
|
|
121
|
+
except RequestError as e:
|
|
122
|
+
return f"Request error during POST request: {e}"
|
|
123
|
+
except Exception as e:
|
|
124
|
+
return f"Error: {str(e) or repr(e)}"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def main():
|
|
128
|
+
mcp.run()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
if __name__ == "__main__":
|
|
132
|
+
main()
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from lxml.html.clean import Cleaner
|
|
3
|
+
from lxml.html import defs, fromstring, tostring
|
|
4
|
+
import re
|
|
5
|
+
from markdownify import markdownify as md
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_auth_from_env() -> tuple[str, str]:
|
|
9
|
+
"""Gets Username and password from enviroment variables"""
|
|
10
|
+
USERNAME = os.getenv('OXYLABS_USERNAME')
|
|
11
|
+
PASSWORD = os.getenv('OXYLABS_PASSWORD')
|
|
12
|
+
|
|
13
|
+
if not USERNAME or not PASSWORD:
|
|
14
|
+
raise ValueError(
|
|
15
|
+
"OXYLABS_USERNAME and OXYLABS_PASSWORD "
|
|
16
|
+
"must be set in the environment variables."
|
|
17
|
+
)
|
|
18
|
+
return USERNAME, PASSWORD
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def clean_html(html: str):
|
|
22
|
+
cleaner = Cleaner(
|
|
23
|
+
scripts=True,
|
|
24
|
+
javascript=True,
|
|
25
|
+
style=True,
|
|
26
|
+
remove_tags=[],
|
|
27
|
+
kill_tags=["nav", "svg", "footer", "noscript", "script", "form"],
|
|
28
|
+
safe_attrs=list(defs.safe_attrs) + ["idx"],
|
|
29
|
+
comments=True,
|
|
30
|
+
inline_style=True,
|
|
31
|
+
links=True,
|
|
32
|
+
meta=False,
|
|
33
|
+
page_structure=False,
|
|
34
|
+
embedded=True,
|
|
35
|
+
frames=False,
|
|
36
|
+
forms=False,
|
|
37
|
+
annoying_tags=False,
|
|
38
|
+
)
|
|
39
|
+
return cleaner.clean_html(html)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def strip_html(html: str) -> str:
|
|
43
|
+
"""
|
|
44
|
+
Cleans and simplifies an HTML string by removing unwanted elements,
|
|
45
|
+
attributes, and redundant content.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
html (str): The input HTML string.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
str: The cleaned and simplified HTML string.
|
|
52
|
+
"""
|
|
53
|
+
cleaned_html = clean_html(html)
|
|
54
|
+
html_tree = fromstring(cleaned_html)
|
|
55
|
+
|
|
56
|
+
for element in html_tree.iter():
|
|
57
|
+
# Remove style attributes.
|
|
58
|
+
if "style" in element.attrib:
|
|
59
|
+
del element.attrib["style"]
|
|
60
|
+
|
|
61
|
+
# Remove elements that have no attributes, no content and no children.
|
|
62
|
+
if (
|
|
63
|
+
(
|
|
64
|
+
not element.attrib
|
|
65
|
+
or (len(element.attrib) == 1 and "idx" in element.attrib)
|
|
66
|
+
)
|
|
67
|
+
and not element.getchildren()
|
|
68
|
+
and (not element.text or not element.text.strip())
|
|
69
|
+
and (not element.tail or not element.tail.strip())
|
|
70
|
+
):
|
|
71
|
+
parent = element.getparent()
|
|
72
|
+
if parent is not None:
|
|
73
|
+
parent.remove(element)
|
|
74
|
+
|
|
75
|
+
# Remove elements with footer and hidden in class or id
|
|
76
|
+
xpath_query = (
|
|
77
|
+
".//*[contains(@class, 'footer') or contains(@id, 'footer') or "
|
|
78
|
+
"contains(@class, 'hidden') or contains(@id, 'hidden')]")
|
|
79
|
+
elements_to_remove = html_tree.xpath(xpath_query)
|
|
80
|
+
for element in elements_to_remove:
|
|
81
|
+
parent = element.getparent()
|
|
82
|
+
if parent is not None:
|
|
83
|
+
parent.remove(element)
|
|
84
|
+
|
|
85
|
+
# Serialize the HTML tree back to a string
|
|
86
|
+
stripped_html = tostring(html_tree, encoding="unicode")
|
|
87
|
+
# Previous cleaning produces empty spaces.
|
|
88
|
+
# Replace multiple spaces with an single one
|
|
89
|
+
stripped_html = re.sub(r"\s{2,}", " ", stripped_html)
|
|
90
|
+
# Replace consecutive newlines with an empty string
|
|
91
|
+
stripped_html = re.sub(r"\n{2,}", "", stripped_html)
|
|
92
|
+
return stripped_html
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def convert_html_to_md(html: str) -> str:
|
|
96
|
+
return md(html)
|
|
File without changes
|
|
File without changes
|