# Directory Structure
```
├── .editorconfig
├── .env.example
├── .github
│ ├── scripts
│ │ └── before-beta-release.cjs
│ └── workflows
│ ├── check.yaml
│ ├── pre_release.yaml
│ └── release.yaml
├── .gitignore
├── .npmignore
├── CHANGELOG.md
├── eslint.config.mjs
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── src
│ ├── example_call_web_browser.ts
│ ├── example_client_stdio.ts
│ ├── index.ts
│ └── server.ts
├── tsconfig.eslint.json
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
```
1 | APIFY_TOKEN=
2 |
```
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
```
1 | # .npmignore
2 | # Exclude everything by default
3 | *
4 |
5 | # Include specific files and folders
6 | !dist/
7 | !README.md
8 | !LICENSE
9 |
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | # This file tells Git which files shouldn't be added to source control
2 |
3 | .DS_Store
4 | .idea
5 | dist
6 | build
7 | node_modules
8 | apify_storage
9 | storage
10 |
11 |
12 | .env
13 |
```
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
```
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 4
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 | insert_final_newline = true
9 | end_of_line = lf
10 | max_line_length = 120
11 |
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # MCP Server for the RAG Web Browser Actor 🌐
2 |
3 | Implementation of an MCP server for the [RAG Web Browser Actor](https://apify.com/apify/rag-web-browser).
4 | This Actor serves as a web browser for large language models (LLMs) and RAG pipelines, similar to a web search in ChatGPT.
5 |
6 | > **This MCP server is deprecated in favor of [mcp.apify.com](https://mcp.apify.com)**
7 | >
8 | > For the same functionality and much more, please use one of these alternatives:
9 |
10 | ## 🚀 Recommended: use mcp.apify.com
11 |
12 | The easiest way to get the same web browsing capabilities is to use **[mcp.apify.com](https://mcp.apify.com)** with default settings.
13 |
14 | **Benefits:**
15 | - ✅ No local setup required
16 | - ✅ Always up-to-date
17 | - ✅ Access to 6,000+ Apify Actors (including RAG Web Browser)
18 | - ✅ OAuth support for easy connection
19 | - ✅ Dynamic tool discovery
20 |
21 | **Quick Setup:**
22 | 1. Go to https://mcp.apify.com
23 | 2. Authorize the client (Claude, VS Code, etc.)
24 | 3. Copy the generated MCP server configuration (or use OAuth flow if supported)
25 | 4. Start using browsing & other tools immediately
26 |
27 | ## 🌐 Alternative: direct RAG Web Browser integration
28 |
29 | You can also call the [RAG Web Browser Actor](https://apify.com/apify/rag-web-browser) directly via its HTTP/SSE interface.
30 |
31 | **Benefits:**
32 | - ✅ Direct integration without mcp.apify.com
33 | - ✅ Real-time streaming via Server-Sent Events
34 | - ✅ Full control over the integration
35 | - ✅ No additional dependencies
36 |
37 | **Docs:** [Actor Documentation](https://apify.com/apify/rag-web-browser#anthropic-model-context-protocol-mcp-server)
38 |
39 | ---
40 |
41 | ## 🎯 What does this MCP server do?
42 |
43 | This server is specifically designed to provide fast responses to AI agents and LLMs, allowing them to interact with the web and extract information from web pages.
44 | It runs locally and communicates with the [RAG Web Browser Actor](https://apify.com/apify/rag-web-browser) in [**Standby mode**](https://docs.apify.com/platform/actors/running/standby),
45 | sending search queries and receiving extracted web content in response.
46 |
47 | - **Web Search**: Query Google Search, scrape top N URLs, and return cleaned content as Markdown
48 | - **Single URL Fetching**: Fetch a specific URL and return its content as Markdown
49 | - **Local MCP Integration**: Standard input/output (stdio) communication with AI clients
50 |
51 | ## 🧱 Components
52 |
53 | ### Tools
54 |
55 | - name: `search`
56 | description: Query Google Search OR fetch a direct URL and return cleaned page contents.
57 | arguments:
58 | - `query` (string, required): Search keywords or a full URL. Advanced Google operators supported.
59 | - `maxResults` (number, optional, default: 1): Max organic results to fetch (ignored when `query` is a URL).
60 | - `scrapingTool` (string, optional, default: `raw-http`): One of `browser-playwright` | `raw-http`.
61 | - `raw-http`: Fast (no JS execution) – good for static pages.
62 | - `browser-playwright`: Handles JS-heavy sites – slower, more robust.
63 | - `outputFormats` (array of strings, optional, default: [`markdown`]): One or more of `text`, `markdown`, `html`.
64 | - `requestTimeoutSecs` (number, optional, default: 40, min 1 max 300): Total server-side AND client wait budget. A local abort is enforced.
65 |
66 |
67 | ## 🔄 Migration Guide
68 |
69 | ### From Local MCP Server to mcp.apify.com
70 |
71 | **Before (Deprecated local server):**
72 | ```json
73 | {
74 | "mcpServers": {
75 | "rag-web-browser": {
76 | "command": "npx",
77 | "args": ["@apify/mcp-server-rag-web-browser"],
78 | "env": {
79 | "APIFY_TOKEN": "your-apify-api-token"
80 | }
81 | }
82 | }
83 | }
84 | ```
85 |
86 | **After (Recommended Apify server):**
87 | ```json
88 | {
89 | "mcpServers": {
90 | "apify": {
91 | "command": "npx",
92 | "args": ["@apify/actors-mcp-server"],
93 | "env": {
94 | "APIFY_TOKEN": "your-apify-api-token"
95 | }
96 | }
97 | }
98 | }
99 | ```
100 | Or use the hosted endpoint: `https://mcp.apify.com` (when your client supports HTTP transport / remote MCP).
101 |
102 | ### MCP clients
103 | - Claude Desktop: https://claude.ai/download
104 | - Visual Studio Code
105 | - Apify Tester MCP Client: https://apify.com/jiri.spilka/tester-mcp-client
106 |
107 | ## 🛠️ Development
108 |
109 | ### Prerequisites
110 | - Node.js (v18 or higher)
111 | - Apify API Token (`APIFY_TOKEN`)
112 |
113 | Clone & install:
114 | ```bash
115 | git clone https://github.com/apify/mcp-server-rag-web-browser.git
116 | cd mcp-server-rag-web-browser
117 | npm install
118 | ```
119 |
120 | ### Build
121 | ```bash
122 | npm install
123 | npm run build
124 | ```
125 |
126 | ### Debugging
127 |
128 | Since MCP servers operate over standard input/output (stdio), debugging can be challenging.
129 | For the best debugging experience, use the [MCP Inspector](https://github.com/modelcontextprotocol/inspector).
130 |
131 | You can launch the MCP Inspector via [`npm`](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) with this command:
132 |
133 | ```bash
134 | export APIFY_TOKEN=your-apify-api-token
135 | npx @modelcontextprotocol/inspector node dist/index.js
136 | ```
137 | Upon launching, the Inspector will display a URL that you can access in your browser to begin debugging.
138 |
139 | # 📖 Learn more
140 |
141 | - [Model Context Protocol](https://modelcontextprotocol.org/)
142 | - [RAG Web Browser Actor](https://apify.com/apify/rag-web-browser)
143 | - [What are AI Agents?](https://blog.apify.com/what-are-ai-agents/)
144 | - [What is MCP and why does it matter?](https://blog.apify.com/what-is-model-context-protocol/)
145 | - [How to use MCP with Apify Actors](https://blog.apify.com/how-to-use-mcp/)
146 | - [Tester MCP Client](https://apify.com/jiri.spilka/tester-mcp-client)
147 | - [Webinar: Building and Monetizing MCP Servers on Apify](https://www.youtube.com/watch?v=w3AH3jIrXXo)
148 | - [How to build and monetize an AI agent on Apify](https://blog.apify.com/how-to-build-an-ai-agent/)
149 | - [Build and deploy MCP servers in minutes with a TypeScript template](https://blog.apify.com/build-and-deploy-mcp-servers-typescript/)
150 |
151 | *This repository is maintained for archival purposes only. Please use the recommended alternatives above for active development.*
152 |
```
--------------------------------------------------------------------------------
/tsconfig.eslint.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "extends": "./tsconfig.json",
3 | "include": [
4 | "src/**/*.ts",
5 | "tests/**/*.ts",
6 | "*.ts",
7 | "*.js",
8 | ".eslintrc.js"
9 | ],
10 | "exclude": [
11 | "node_modules",
12 | "dist"
13 | ]
14 | }
15 |
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "extends": "@apify/tsconfig",
3 | "compilerOptions": {
4 | "module": "ESNext",
5 | "target": "ESNext",
6 | "outDir": "dist",
7 | "moduleResolution": "node",
8 | "noUnusedLocals": false,
9 | "lib": ["ES2022"],
10 | "skipLibCheck": true,
11 | "typeRoots": ["./types", "./node_modules/@types"],
12 | "strict": true
13 | },
14 | "include": ["./src/**/*"],
15 | "exclude": ["node_modules"]
16 | }
17 |
```
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
```
1 | import apifyConfig from '@apify/eslint-config';
2 |
3 | // eslint-disable-next-line import/no-default-export
4 | export default [
5 | { ignores: ['**/dist'] }, // Ignores need to happen first
6 | ...(Array.isArray(apifyConfig) ? apifyConfig : [apifyConfig]),
7 | {
8 | languageOptions: {
9 | sourceType: 'module',
10 |
11 | parserOptions: {
12 | project: 'tsconfig.eslint.json',
13 | },
14 | },
15 | },
16 | ];
17 |
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | #!/usr/bin/env node
2 | /**
3 | * This script initializes and starts the MCP server for the Apify RAG Web Browser using the Stdio transport.
4 | *
5 | * Usage:
6 | * node <script_name>
7 | *
8 | * Example:
9 | * node index.js
10 | */
11 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
12 |
13 | import { RagWebBrowserServer } from './server.js';
14 |
15 | async function main() {
16 | const server = new RagWebBrowserServer();
17 | const transport = new StdioServerTransport();
18 | await server.connect(transport);
19 | }
20 |
21 | main().catch((error) => {
22 | console.error('Server error:', error); // eslint-disable-line no-console
23 | process.exit(1);
24 | });
25 |
```
--------------------------------------------------------------------------------
/.github/workflows/check.yaml:
--------------------------------------------------------------------------------
```yaml
1 | # This workflow runs for every pull request to lint and test the proposed changes.
2 |
3 | name: Check
4 |
5 | on:
6 | pull_request:
7 |
8 | # Push to main will trigger code checks
9 | push:
10 | branches:
11 | - main
12 | tags-ignore:
13 | - "**" # Ignore all tags to prevent duplicate builds when tags are pushed.
14 |
15 | jobs:
16 | lint_and_test:
17 | name: Lint
18 | runs-on: ubuntu-latest
19 |
20 | steps:
21 | - uses: actions/checkout@v4
22 | - name: Use Node.js 22
23 | uses: actions/setup-node@v4
24 | with:
25 | node-version: 22
26 | cache: 'npm'
27 | cache-dependency-path: 'package-lock.json'
28 | - name: Install Dependencies
29 | run: npm ci
30 |
31 | - name: Lint
32 | run: npm run lint
33 |
```
--------------------------------------------------------------------------------
/.github/scripts/before-beta-release.cjs:
--------------------------------------------------------------------------------
```
1 | const { execSync } = require('child_process');
2 | const fs = require('fs');
3 | const path = require('path');
4 |
5 | const PKG_JSON_PATH = path.join(__dirname, '..', '..', 'package.json');
6 |
7 | const pkgJson = require(PKG_JSON_PATH); // eslint-disable-line import/no-dynamic-require
8 |
9 | const PACKAGE_NAME = pkgJson.name;
10 | const VERSION = pkgJson.version;
11 |
12 | const nextVersion = getNextVersion(VERSION);
13 | console.log(`before-deploy: Setting version to ${nextVersion}`); // eslint-disable-line no-console
14 | pkgJson.version = nextVersion;
15 |
16 | fs.writeFileSync(PKG_JSON_PATH, `${JSON.stringify(pkgJson, null, 2)}\n`);
17 |
18 | function getNextVersion(version) {
19 | const versionString = execSync(`npm show ${PACKAGE_NAME} versions --json`, { encoding: 'utf8' });
20 | const versions = JSON.parse(versionString);
21 |
22 | if (versions.some((v) => v === VERSION)) {
23 | console.error(`before-deploy: A release with version ${VERSION} already exists. Please increment version accordingly.`); // eslint-disable-line no-console
24 | process.exit(1);
25 | }
26 |
27 | const prereleaseNumbers = versions
28 | .filter((v) => (v.startsWith(VERSION) && v.includes('-')))
29 | .map((v) => Number(v.match(/\.(\d+)$/)[1]));
30 | const lastPrereleaseNumber = Math.max(-1, ...prereleaseNumbers);
31 | return `${version}-beta.${lastPrereleaseNumber + 1}`;
32 | }
33 |
```
--------------------------------------------------------------------------------
/src/example_call_web_browser.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Example how to call the RAG Web Browser Actor in a standby mode.
3 | * @module src/example_call_web_browser
4 | */
5 |
6 | /* eslint-disable no-console */
7 | import dotenv from 'dotenv';
8 | import fetch from 'node-fetch';
9 |
10 | dotenv.config({ path: '../.env' });
11 |
12 | const QUERY = 'MCP Server for Anthropic';
13 | const MAX_RESULTS = 1; // Limit the number of results to decrease response size
14 | const ACTOR_BASE_URL = 'https://rag-web-browser.apify.actor/search'; // Base URL from OpenAPI schema
15 |
16 | const { APIFY_TOKEN } = process.env;
17 |
18 | if (!APIFY_TOKEN) {
19 | throw new Error('APIFY_TOKEN environment variable is not set.');
20 | }
21 |
22 | const queryParams = new URLSearchParams({
23 | query: QUERY,
24 | maxResults: MAX_RESULTS.toString(),
25 | });
26 |
27 | const headers = {
28 | Authorization: `Bearer ${APIFY_TOKEN}`,
29 | };
30 |
31 | // eslint-disable-next-line no-void
32 | void (async () => {
33 | const url = `${ACTOR_BASE_URL}?${queryParams.toString()}`;
34 | console.info(`GET request to ${url}`);
35 |
36 | try {
37 | const response = await fetch(url, { method: 'GET', headers });
38 |
39 | if (!response.ok) {
40 | console.log(`Error: Failed to fetch data: ${response.statusText}`);
41 | }
42 |
43 | const responseBody = await response.json();
44 | console.info('Received response from RAG Web Browser:', responseBody);
45 |
46 | // Optional: Further process or display the response
47 | console.log('Response:', responseBody);
48 | } catch (error: any) { // eslint-disable-line @typescript-eslint/no-explicit-any
49 | console.error('Error occurred:', error.message);
50 | }
51 | })();
52 |
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "@apify/mcp-server-rag-web-browser",
3 | "version": "0.1.4",
4 | "type": "module",
5 | "description": "An MCP Server for RAG Web Browser Actor",
6 | "engines": {
7 | "node": ">=18.0.0"
8 | },
9 | "main": "dist/index.js",
10 | "bin": {
11 | "mcp-rag-web-browser": "./dist/index.js"
12 | },
13 | "files": [
14 | "dist",
15 | "LICENSE"
16 | ],
17 | "repository": {
18 | "type": "git",
19 | "url": "https://github.com/apify/mcp-server-rag-web-browser"
20 | },
21 | "bugs": {
22 | "url": "https://github.com/apify/mcp-server-rag-web-browser/issues"
23 | },
24 | "homepage": "https://github.com/apify/mcp-server-rag-web-browser",
25 | "keywords": [
26 | "apify",
27 | "mcp",
28 | "server",
29 | "actors",
30 | "model context protocol",
31 | "rag",
32 | "web browser"
33 | ],
34 | "scripts": {
35 | "start": "npm run start:dev",
36 | "start:prod": "node dist/index.js",
37 | "start:dev": "tsx src/index.ts",
38 | "lint": "eslint .",
39 | "lint:fix": "eslint . --fix",
40 | "build": "tsc",
41 | "watch": "tsc --watch",
42 | "inspector": "npx @modelcontextprotocol/inspector dist/index.js"
43 | },
44 | "dependencies": {
45 | "@modelcontextprotocol/sdk": "^1.0.4",
46 | "express": "^4.21.2",
47 | "node-fetch": "^3.3.2",
48 | "zod": "^3.24.1",
49 | "zod-to-json-schema": "^3.24.1"
50 | },
51 | "devDependencies": {
52 | "@apify/eslint-config": "^0.5.0-beta.7",
53 | "@apify/eslint-config-ts": "^0.4.1",
54 | "@apify/tsconfig": "^0.1.0",
55 | "@types/express": "^5.0.0",
56 | "@types/node": "^20.11.24",
57 | "dotenv": "^16.4.7",
58 | "eslint": "^9.22.0",
59 | "tsx": "^4.6.2",
60 | "typescript": "^5.3.3",
61 | "typescript-eslint": "^8.18.2"
62 | }
63 | }
64 |
```
--------------------------------------------------------------------------------
/src/example_client_stdio.ts:
--------------------------------------------------------------------------------
```typescript
1 | /* eslint-disable no-console */
2 | /**
3 | * Connect to the MCP server using stdio transport and call a tool.
4 | * You need provide a path to MCP server and APIFY_TOKEN in .env file.
5 | */
6 |
7 | import { Client } from '@modelcontextprotocol/sdk/client/index.js';
8 | import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
9 | import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';
10 | import dotenv from 'dotenv';
11 |
12 | dotenv.config({ path: '../.env' });
13 |
14 | const { APIFY_TOKEN } = process.env;
15 |
16 | if (!APIFY_TOKEN) {
17 | throw new Error('APIFY_TOKEN environment variable is not set.');
18 | }
19 |
20 | const SERVER_PATH = '../dist/index.js';
21 |
22 | // Create server parameters for stdio connection
23 | const transport = new StdioClientTransport({
24 | command: 'node', // Executable
25 | args: [
26 | SERVER_PATH,
27 | ],
28 | env: {
29 | 'APIFY_TOKEN': APIFY_TOKEN
30 | }
31 | });
32 |
33 | // Create a new client instance
34 | const client = new Client(
35 | { name: 'example-client', version: '1.0.0' },
36 | { capabilities: {} },
37 | );
38 |
39 | // Main function to run the example client
40 | async function run() {
41 | try {
42 | // Connect to the MCP server
43 | await client.connect(transport);
44 |
45 | // List available tools
46 | const tools = await client.listTools();
47 | console.log('Available tools:', tools);
48 |
49 | // Call a tool
50 | console.log('Calling rag web browser ...');
51 | const result = await client.callTool(
52 | { name: 'search', arguments: { query: 'web browser for Anthropic' } },
53 | CallToolResultSchema,
54 | );
55 | console.log('Tool result:', JSON.stringify(result));
56 | } catch (error) {
57 | console.error('Error:', error);
58 | }
59 | }
60 |
61 | // Execute the main function
62 | await run();
63 |
```
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
```markdown
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | ## [0.1.4](https://github.com/apify/mcp-server-rag-web-browser/releases/tag/v0.1.4) (2025-08-21)
6 |
7 | ### 🚀 Features
8 |
9 | - Update readme ([#22](https://github.com/apify/mcp-server-rag-web-browser/pull/22)) ([57218e5](https://github.com/apify/mcp-server-rag-web-browser/commit/57218e50b7378b45713097d61615da71d9740a3e)) by [@jirispilka](https://github.com/jirispilka)
10 |
11 |
12 | ## [0.1.3](https://github.com/apify/mcp-server-rag-web-browser/releases/tag/v0.1.3) (2025-03-17)
13 |
14 | ### 🐛 Bug Fixes
15 |
16 | - Changelog, package ([#17](https://github.com/apify/mcp-server-rag-web-browser/pull/17)) ([9067509](https://github.com/apify/mcp-server-rag-web-browser/commit/9067509a12fda837456899b22a3432c8ff172f4f)) by [@jirispilka](https://github.com/jirispilka)
17 |
18 |
19 | ## [0.1.1](https://github.com/apify/mcp-server-rag-web-browser/releases/tag/v0.1.1) (2025-03-17)
20 |
21 | ### 🚀 Features
22 | - Add scraping tool option ([#13](https://github.com/apify/mcp-server-rag-web-browser/pull/13)) ([0dc796c](https://github.com/apify/mcp-server-rag-web-browser/commit/0dc796cea98e02e276fcc03e43514fa156a3018d)) by [@jirispilka](https://github.com/jirispilka)
23 | - Refactor server to simplify it ([#14](https://github.com/apify/mcp-server-rag-web-browser/pull/14)) ([bbd063b](https://github.com/apify/mcp-server-rag-web-browser/commit/bbd063b2b4fc58e1fd25c07908ec1e8355955c59)) by [@jirispilka](https://github.com/jirispilka)
24 | - Change env variable name from APIFY_API_TOKEN to APIFY_TOKEN
25 | - Add extra query parameters
26 | - Remove unnecessary example clients (SSE and chat client)
27 | - Create NPM package @apify/mcp-server-rag-web-browser
28 | - Update readme
29 |
30 | ### 🐛 Bug Fixes
31 |
32 | - Workflow ([#16](https://github.com/apify/mcp-server-rag-web-browser/pull/16)) ([dea430d](https://github.com/apify/mcp-server-rag-web-browser/commit/dea430d793e924821b3eebc99ed4996333af99b8)) by [@jirispilka](https://github.com/jirispilka)
33 |
34 | ## [0.1.0](https://github.com/apify/mcp-server-rag-web-browser/releases/tag/v0.1.0) (2025-03-17)
35 |
36 | ### 🚀 Features
37 |
38 | - Initial release
39 | - Example clients ([#10](https://github.com/apify/mcp-server-rag-web-browser/pull/10)) ([ad31f34](https://github.com/apify/mcp-server-rag-web-browser/commit/ad31f34045e3d5a01b41073af06bae33e89b1f32)) by [@jirispilka](https://github.com/jirispilka), closes [#8](https://github.com/apify/mcp-server-rag-web-browser/issues/8)
40 |
41 | ### 🐛 Bug Fixes
42 |
43 | - Update version ([#15](https://github.com/apify/mcp-server-rag-web-browser/pull/15)) ([1a05865](https://github.com/apify/mcp-server-rag-web-browser/commit/1a05865b4052b080a0dd758ec0e90ea1f7bdd14c)) by [@jirispilka](https://github.com/jirispilka)
```
--------------------------------------------------------------------------------
/.github/workflows/pre_release.yaml:
--------------------------------------------------------------------------------
```yaml
1 | name: Create a pre-release
2 |
3 | on:
4 | # Push to main will deploy a beta version
5 | push:
6 | branches:
7 | - main
8 | tags-ignore:
9 | - "**" # Ignore all tags to prevent duplicate builds when tags are pushed.
10 |
11 | concurrency:
12 | group: release
13 | cancel-in-progress: false
14 |
15 | jobs:
16 | release_metadata:
17 | if: "!startsWith(github.event.head_commit.message, 'docs') && !startsWith(github.event.head_commit.message, 'ci') && startsWith(github.repository, 'apify/')"
18 | name: Prepare release metadata
19 | runs-on: ubuntu-latest
20 | outputs:
21 | version_number: ${{ steps.release_metadata.outputs.version_number }}
22 | changelog: ${{ steps.release_metadata.outputs.changelog }}
23 | steps:
24 | - uses: apify/workflows/git-cliff-release@main
25 | name: Prepare release metadata
26 | id: release_metadata
27 | with:
28 | release_type: prerelease
29 | existing_changelog_path: CHANGELOG.md
30 |
31 | wait_for_checks:
32 | name: Wait for code checks to pass
33 | runs-on: ubuntu-latest
34 | steps:
35 | - uses: lewagon/[email protected]
36 | with:
37 | ref: ${{ github.ref }}
38 | repo-token: ${{ secrets.GITHUB_TOKEN }}
39 | check-regexp: (Build & Test .*|Lint|Docs build)
40 | wait-interval: 5
41 |
42 | update_changelog:
43 | needs: [ release_metadata, wait_for_checks ]
44 | name: Update changelog
45 | runs-on: ubuntu-latest
46 | outputs:
47 | changelog_commitish: ${{ steps.commit.outputs.commit_long_sha || github.sha }}
48 |
49 | steps:
50 | - name: Checkout repository
51 | uses: actions/checkout@v4
52 | with:
53 | token: ${{ secrets.APIFY_SERVICE_ACCOUNT_GITHUB_TOKEN }}
54 |
55 | - name: Use Node.js 22
56 | uses: actions/setup-node@v4
57 | with:
58 | node-version: 22
59 |
60 | - name: Update package version in package.json
61 | run: npm version --no-git-tag-version --allow-same-version ${{ needs.release_metadata.outputs.version_number }}
62 |
63 | - name: Update CHANGELOG.md
64 | uses: DamianReeves/write-file-action@master
65 | with:
66 | path: CHANGELOG.md
67 | write-mode: overwrite
68 | contents: ${{ needs.release_metadata.outputs.changelog }}
69 |
70 | - name: Commit changes
71 | id: commit
72 | uses: EndBug/add-and-commit@v9
73 | with:
74 | author_name: Apify Release Bot
75 | author_email: [email protected]
76 | message: "chore(release): Update changelog and package version [skip ci]"
77 |
78 | publish_to_npm:
79 | name: Publish to NPM
80 | needs: [ release_metadata, wait_for_checks ]
81 | runs-on: ubuntu-latest
82 | steps:
83 | - uses: actions/checkout@v4
84 | with:
85 | ref: ${{ needs.update_changelog.changelog_commitish }}
86 | - name: Use Node.js 22
87 | uses: actions/setup-node@v4
88 | with:
89 | node-version: 22
90 | cache: 'npm'
91 | cache-dependency-path: 'package-lock.json'
92 | - name: Install dependencies
93 | run: |
94 | echo "access=public" >> .npmrc
95 | echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" >> .npmrc
96 | npm ci
97 | - # Check version consistency and increment pre-release version number for beta only.
98 | name: Bump pre-release version
99 | run: node ./.github/scripts/before-beta-release.cjs
100 | - name: Build module
101 | run: npm run build
102 | - name: Publish to NPM
103 | run: npm publish --tag beta
104 |
105 | env:
106 | NODE_AUTH_TOKEN: ${{ secrets.APIFY_SERVICE_ACCOUNT_NPM_TOKEN }}
107 | NPM_TOKEN: ${{ secrets.APIFY_SERVICE_ACCOUNT_NPM_TOKEN }}
108 |
```
--------------------------------------------------------------------------------
/.github/workflows/release.yaml:
--------------------------------------------------------------------------------
```yaml
1 | name: Create a release
2 |
3 | on:
4 | # Trigger a stable version release via GitHub's UI, with the ability to specify the type of release.
5 | workflow_dispatch:
6 | inputs:
7 | release_type:
8 | description: Release type
9 | required: true
10 | type: choice
11 | default: auto
12 | options:
13 | - auto
14 | - custom
15 | - patch
16 | - minor
17 | - major
18 | custom_version:
19 | description: The custom version to bump to (only for "custom" type)
20 | required: false
21 | type: string
22 | default: ""
23 |
24 | concurrency:
25 | group: release
26 | cancel-in-progress: false
27 |
28 | jobs:
29 | release_metadata:
30 | name: Prepare release metadata
31 | runs-on: ubuntu-latest
32 | outputs:
33 | version_number: ${{ steps.release_metadata.outputs.version_number }}
34 | tag_name: ${{ steps.release_metadata.outputs.tag_name }}
35 | changelog: ${{ steps.release_metadata.outputs.changelog }}
36 | release_notes: ${{ steps.release_metadata.outputs.release_notes }}
37 | steps:
38 | - uses: apify/workflows/git-cliff-release@main
39 | name: Prepare release metadata
40 | id: release_metadata
41 | with:
42 | release_type: ${{ inputs.release_type }}
43 | custom_version: ${{ inputs.custom_version }}
44 | existing_changelog_path: CHANGELOG.md
45 |
46 | update_changelog:
47 | needs: [ release_metadata ]
48 | name: Update changelog
49 | runs-on: ubuntu-latest
50 | outputs:
51 | changelog_commitish: ${{ steps.commit.outputs.commit_long_sha || github.sha }}
52 |
53 | steps:
54 | - name: Checkout repository
55 | uses: actions/checkout@v4
56 | with:
57 | token: ${{ secrets.APIFY_SERVICE_ACCOUNT_GITHUB_TOKEN }}
58 |
59 | - name: Use Node.js 22
60 | uses: actions/setup-node@v4
61 | with:
62 | node-version: 22
63 |
64 | - name: Update package version in package.json
65 | run: npm version --no-git-tag-version --allow-same-version ${{ needs.release_metadata.outputs.version_number }}
66 |
67 | - name: Update CHANGELOG.md
68 | uses: DamianReeves/write-file-action@master
69 | with:
70 | path: CHANGELOG.md
71 | write-mode: overwrite
72 | contents: ${{ needs.release_metadata.outputs.changelog }}
73 |
74 | - name: Commit changes
75 | id: commit
76 | uses: EndBug/add-and-commit@v9
77 | with:
78 | author_name: Apify Release Bot
79 | author_email: [email protected]
80 | message: "chore(release): Update changelog and package version [skip ci]"
81 |
82 | create_github_release:
83 | name: Create github release
84 | needs: [release_metadata, update_changelog]
85 | runs-on: ubuntu-latest
86 | env:
87 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
88 | steps:
89 | - name: Create release
90 | uses: softprops/action-gh-release@v2
91 | with:
92 | tag_name: ${{ needs.release_metadata.outputs.tag_name }}
93 | name: ${{ needs.release_metadata.outputs.version_number }}
94 | target_commitish: ${{ needs.update_changelog.outputs.changelog_commitish }}
95 | body: ${{ needs.release_metadata.outputs.release_notes }}
96 |
97 | publish_to_npm:
98 | name: Publish to NPM
99 | needs: [ update_changelog ]
100 | runs-on: ubuntu-latest
101 | steps:
102 | - uses: actions/checkout@v4
103 | with:
104 | ref: ${{ needs.update_changelog.changelog_commitish }}
105 | - name: Use Node.js 22
106 | uses: actions/setup-node@v4
107 | with:
108 | node-version: 22
109 | cache: 'npm'
110 | cache-dependency-path: 'package-lock.json'
111 | - name: Install dependencies
112 | run: |
113 | echo "access=public" >> .npmrc
114 | echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" >> .npmrc
115 | npm ci
116 | - name: Build module
117 | run: npm run build
118 | - name: Publish to NPM
119 | run: npm publish --tag latest
120 |
121 | env:
122 | NODE_AUTH_TOKEN: ${{ secrets.APIFY_SERVICE_ACCOUNT_NPM_TOKEN }}
123 | NPM_TOKEN: ${{ secrets.APIFY_SERVICE_ACCOUNT_NPM_TOKEN }}
124 |
```
--------------------------------------------------------------------------------
/src/server.ts:
--------------------------------------------------------------------------------
```typescript
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * MCP server that allows to call the RAG Web Browser Actor
5 | */
6 |
7 | import { Server } from '@modelcontextprotocol/sdk/server/index.js';
8 | import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
9 | import {
10 | CallToolRequestSchema,
11 | ListToolsRequestSchema,
12 | } from '@modelcontextprotocol/sdk/types.js';
13 | import { z } from 'zod';
14 | import { zodToJsonSchema } from 'zod-to-json-schema';
15 |
16 |
17 | const { APIFY_TOKEN } = process.env;
18 |
19 | const MAX_RESULTS = 1;
20 | const TOOL_SEARCH = 'search';
21 | const ACTOR_BASE_URL = 'https://rag-web-browser.apify.actor/search';
22 |
23 | const WebBrowserArgsSchema = z.object({
24 | query: z.string()
25 | .describe('Enter Google Search keywords or a URL of a specific web page. The keywords might include the'
26 | + 'advanced search operators. Examples: "san francisco weather", "https://www.cnn.com", '
27 | + '"function calling site:openai.com"')
28 | .regex(/[^\s]+/, { message: "Search term or URL cannot be empty" }),
29 | maxResults: z.number().int().positive().min(1).max(100).default(MAX_RESULTS)
30 | .describe(
31 | 'The maximum number of top organic Google Search results whose web pages will be extracted. '
32 | + 'If query is a URL, then this field is ignored and the Actor only fetches the specific web page.',
33 | ),
34 | scrapingTool: z.enum(['browser-playwright', 'raw-http'])
35 | .describe('Select a scraping tool for extracting the target web pages. '
36 | + 'The Browser tool is more powerful and can handle JavaScript heavy websites, while the '
37 | + 'Plain HTML tool can not handle JavaScript but is about two times faster.')
38 | .default('raw-http'),
39 | outputFormats: z.array(z.enum(['text', 'markdown', 'html']))
40 | .describe('Select one or more formats to which the target web pages will be extracted.')
41 | .default(['markdown']),
42 | requestTimeoutSecs: z.number().int().min(1).max(300).default(40)
43 | .describe('The maximum time in seconds available for the request, including querying Google Search '
44 | + 'and scraping the target web pages.'),
45 | });
46 |
47 | /**
48 | * Create an MCP server with a tool to call RAG Web Browser Actor
49 | */
50 | export class RagWebBrowserServer {
51 | private server: Server;
52 |
53 | constructor() {
54 | this.server = new Server(
55 | {
56 | name: 'mcp-server-rag-web-browser',
57 | version: '0.1.0',
58 | },
59 | {
60 | capabilities: {
61 | tools: {},
62 | },
63 | },
64 | );
65 | this.setupErrorHandling();
66 | this.setupToolHandlers();
67 | }
68 |
69 | private async callRagWebBrowser(args: z.infer<typeof WebBrowserArgsSchema>): Promise<string> {
70 | if (!APIFY_TOKEN) {
71 | throw new Error('APIFY_TOKEN is required but not set. '
72 | + 'Please set it in your environment variables or pass it as a command-line argument.');
73 | }
74 |
75 | const queryParams = new URLSearchParams({
76 | query: args.query,
77 | maxResults: args.maxResults.toString(),
78 | scrapingTool: args.scrapingTool,
79 | });
80 |
81 | // Add all other parameters if provided
82 | if (args.outputFormats) {
83 | args.outputFormats.forEach((format) => {
84 | queryParams.append('outputFormats', format);
85 | });
86 | }
87 | if (args.requestTimeoutSecs) {
88 | queryParams.append('requestTimeoutSecs', args.requestTimeoutSecs.toString());
89 | }
90 |
91 | const url = `${ACTOR_BASE_URL}?${queryParams.toString()}`;
92 | const response = await fetch(url, {
93 | method: 'GET',
94 | headers: {
95 | Authorization: `Bearer ${APIFY_TOKEN}`,
96 | },
97 | });
98 |
99 | if (!response.ok) {
100 | throw new Error(`Failed to call RAG Web Browser: ${response.status} ${response.statusText}`);
101 | }
102 |
103 | const responseBody = await response.json();
104 | return JSON.stringify(responseBody);
105 | }
106 |
107 | private setupErrorHandling(): void {
108 | this.server.onerror = (error) => {
109 | console.error('[MCP Error]', error); // eslint-disable-line no-console
110 | };
111 | process.on('SIGINT', async () => {
112 | await this.server.close();
113 | process.exit(0);
114 | });
115 | }
116 |
117 | private setupToolHandlers(): void {
118 | this.server.setRequestHandler(ListToolsRequestSchema, async () => {
119 | return {
120 | tools: [
121 | {
122 | name: TOOL_SEARCH,
123 | description: 'Search phrase or a URL at Google and return crawled web pages as text or Markdown. '
124 | + 'Prefer HTTP raw client for speed and browser-playwright for reliability.',
125 | inputSchema: zodToJsonSchema(WebBrowserArgsSchema),
126 | },
127 | ],
128 | };
129 | });
130 | this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
131 | const { name, arguments: args } = request.params;
132 | switch (name) {
133 | case TOOL_SEARCH: {
134 | try {
135 | const parsed = WebBrowserArgsSchema.parse(args);
136 | const content = await this.callRagWebBrowser(parsed);
137 | return {
138 | content: [{ type: 'text', text: content }],
139 | };
140 | } catch (error) {
141 | console.error('[MCP Error]', error);
142 | throw new Error(`Failed to call RAG Web Browser: ${error}`);
143 | }
144 | }
145 | default: {
146 | throw new Error(`Unknown tool: ${name}`);
147 | }
148 | }
149 | });
150 | }
151 |
152 | async connect(transport: Transport): Promise<void> {
153 | await this.server.connect(transport);
154 | }
155 | }
156 |
```