cloudflare-browser-rendering-mcp
by amotivv
Verified
- src
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ErrorCode,
ListToolsRequestSchema,
McpError,
} from '@modelcontextprotocol/sdk/types.js';
import { BrowserClient } from './browser-client.js';
import { ContentProcessor } from './content-processor.js';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
/**
* Cloudflare Browser Rendering MCP Server
*
* This server provides tools for fetching and processing web content
* using Cloudflare Browser Rendering for use as context in LLMs.
*/
export class BrowserRenderingServer {
private server: Server;
private browserClient: BrowserClient;
private contentProcessor: ContentProcessor;
private screenshotsDir: string;
constructor() {
// Set up screenshots directory
const __dirname = path.dirname(fileURLToPath(import.meta.url));
this.screenshotsDir = path.join(__dirname, '..', 'screenshots');
if (!fs.existsSync(this.screenshotsDir)) {
fs.mkdirSync(this.screenshotsDir, { recursive: true });
}
this.server = new Server(
{
name: 'cloudflare-browser-rendering',
version: '0.1.0',
},
{
capabilities: {
tools: {},
},
}
);
// Initialize the browser client and content processor
this.browserClient = new BrowserClient();
this.contentProcessor = new ContentProcessor();
// Set up request handlers
this.setupToolHandlers();
// Error handling with enhanced logging
this.server.onerror = (error) => console.error('[Error] MCP protocol error:', error);
process.on('SIGINT', async () => {
console.error('[Setup] Shutting down server due to SIGINT');
await this.server.close();
process.exit(0);
});
}
/**
* Set up tool handlers for the MCP server
* Following MCP protocol standards for both Claude and Cline
*/
private setupToolHandlers() {
console.error('[Setup] Registering tool handlers');
// List available tools
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'fetch_page',
description: 'Fetches and processes a web page for LLM context',
inputSchema: {
type: 'object',
properties: {
url: {
type: 'string',
description: 'URL to fetch',
},
maxContentLength: {
type: 'number',
description: 'Maximum content length to return',
},
},
required: ['url'],
},
},
{
name: 'search_documentation',
description: 'Searches Cloudflare documentation and returns relevant content',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query',
},
maxResults: {
type: 'number',
description: 'Maximum number of results to return',
},
},
required: ['query'],
},
},
{
name: 'extract_structured_content',
description: 'Extracts structured content from a web page using CSS selectors',
inputSchema: {
type: 'object',
properties: {
url: {
type: 'string',
description: 'URL to extract content from',
},
selectors: {
type: 'object',
description: 'CSS selectors to extract content',
additionalProperties: {
type: 'string',
},
},
},
required: ['url', 'selectors'],
},
},
{
name: 'summarize_content',
description: 'Summarizes web content for more concise LLM context',
inputSchema: {
type: 'object',
properties: {
url: {
type: 'string',
description: 'URL to summarize',
},
maxLength: {
type: 'number',
description: 'Maximum length of the summary',
},
},
required: ['url'],
},
},
{
name: 'take_screenshot',
description: 'Takes a screenshot of a web page and returns it as an image',
inputSchema: {
type: 'object',
properties: {
url: {
type: 'string',
description: 'URL to take a screenshot of',
},
width: {
type: 'number',
description: 'Width of the viewport in pixels (default: 1280)',
},
height: {
type: 'number',
description: 'Height of the viewport in pixels (default: 800)',
},
fullPage: {
type: 'boolean',
description: 'Whether to take a screenshot of the full page or just the viewport (default: false)',
},
},
required: ['url'],
},
},
],
}));
// Handle tool calls
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
console.error(`[API] Tool call received: ${name}`);
try {
switch (name) {
case 'fetch_page':
console.error(`[API] Fetching page: ${args?.url}`);
return await this.handleFetchPage(args);
case 'search_documentation':
console.error(`[API] Searching documentation for: ${args?.query}`);
return await this.handleSearchDocumentation(args);
case 'extract_structured_content':
console.error(`[API] Extracting structured content from: ${args?.url}`);
return await this.handleExtractStructuredContent(args);
case 'summarize_content':
console.error(`[API] Summarizing content from: ${args?.url}`);
return await this.handleSummarizeContent(args);
case 'take_screenshot':
console.error(`[API] Taking screenshot of: ${args?.url}`);
return await this.handleTakeScreenshot(args);
default:
throw new McpError(
ErrorCode.MethodNotFound,
`Unknown tool: ${name}`
);
}
} catch (error) {
if (error instanceof McpError) {
console.error(`[Error] MCP error in tool ${name}:`, error);
throw error;
}
console.error(`[Error] Error in tool ${name}:`, error);
throw new McpError(
ErrorCode.InternalError,
`Error executing tool ${name}: ${error instanceof Error ? error.message : String(error)}`
);
}
});
}
/**
* Handle the fetch_page tool
*/
private async handleFetchPage(args: any) {
// Validate arguments
if (typeof args !== 'object' || args === null || typeof args.url !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'Invalid arguments for fetch_page');
}
const { url, maxContentLength = 10000 } = args;
try {
// Fetch the page content
const html = await this.browserClient.fetchContent(url);
// Process the content for LLM
const processedContent = this.contentProcessor.processForLLM(html, url);
// Truncate if necessary
const truncatedContent = processedContent.length > maxContentLength
? processedContent.substring(0, maxContentLength) + '...'
: processedContent;
// Return the content
return {
content: [
{
type: 'text',
text: truncatedContent,
},
],
};
} catch (error) {
console.error('[Error] Error fetching page:', error);
return {
content: [
{
type: 'text',
text: `Error fetching page: ${error instanceof Error ? error.message : String(error)}`,
},
],
isError: true,
};
}
}
/**
* Handle the search_documentation tool
*/
private async handleSearchDocumentation(args: any) {
// Validate arguments
if (typeof args !== 'object' || args === null || typeof args.query !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'Invalid arguments for search_documentation');
}
const { query, maxResults = 3 } = args;
try {
// In a real implementation, you would:
// 1. Use Cloudflare Browser Rendering to navigate to the docs
// 2. Use the search functionality on the docs site
// 3. Extract the search results
// For this simulation, we'll return mock results
const mockResults = [
{
title: 'Browser Rendering API Overview',
url: 'https://developers.cloudflare.com/browser-rendering/',
snippet: 'Cloudflare Browser Rendering is a serverless headless browser service that allows execution of browser actions within Cloudflare Workers.',
},
{
title: 'REST API Reference',
url: 'https://developers.cloudflare.com/browser-rendering/rest-api/',
snippet: 'The REST API provides simple endpoints for common browser tasks like fetching content, taking screenshots, and generating PDFs.',
},
{
title: 'Workers Binding API Reference',
url: 'https://developers.cloudflare.com/browser-rendering/workers-binding/',
snippet: 'For more advanced use cases, you can use the Workers Binding API with Puppeteer to automate browser interactions.',
},
].slice(0, maxResults);
// Format the results
const formattedResults = mockResults.map(result =>
`## [${result.title}](${result.url})\n${result.snippet}\n`
).join('\n');
return {
content: [
{
type: 'text',
text: `# Search Results for "${query}"\n\n${formattedResults}`,
},
],
};
} catch (error) {
console.error('[Error] Error searching documentation:', error);
return {
content: [
{
type: 'text',
text: `Error searching documentation: ${error instanceof Error ? error.message : String(error)}`,
},
],
isError: true,
};
}
}
/**
* Handle the extract_structured_content tool
*/
private async handleExtractStructuredContent(args: any) {
// Validate arguments
if (
typeof args !== 'object' ||
args === null ||
typeof args.url !== 'string' ||
typeof args.selectors !== 'object'
) {
throw new McpError(ErrorCode.InvalidParams, 'Invalid arguments for extract_structured_content');
}
const { url, selectors } = args;
try {
// In a real implementation, you would:
// 1. Use Cloudflare Browser Rendering to fetch the page
// 2. Use the /scrape endpoint to extract content based on selectors
// For this simulation, we'll return mock results
const mockResults: Record<string, string> = {};
for (const [key, selector] of Object.entries(selectors)) {
if (typeof selector === 'string') {
// Simulate extraction based on selector
mockResults[key] = `Extracted content for selector "${selector}"`;
}
}
// Format the results
const formattedResults = Object.entries(mockResults)
.map(([key, value]) => `## ${key}\n${value}`)
.join('\n\n');
return {
content: [
{
type: 'text',
text: `# Structured Content from ${url}\n\n${formattedResults}`,
},
],
};
} catch (error) {
console.error('[Error] Error extracting structured content:', error);
return {
content: [
{
type: 'text',
text: `Error extracting structured content: ${error instanceof Error ? error.message : String(error)}`,
},
],
isError: true,
};
}
}
/**
* Handle the summarize_content tool
*/
private async handleSummarizeContent(args: any) {
// Validate arguments
if (typeof args !== 'object' || args === null || typeof args.url !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'Invalid arguments for summarize_content');
}
const { url, maxLength = 500 } = args;
try {
// In a real implementation, you would:
// 1. Fetch the page content using Cloudflare Browser Rendering
// 2. Process the content for LLM
// 3. Call an LLM API to summarize the content
// For this simulation, we'll return a mock summary
const mockSummary = `
# Browser Rendering API Summary
Cloudflare Browser Rendering is a serverless headless browser service for Cloudflare Workers that enables:
1. Rendering JavaScript-heavy websites
2. Taking screenshots and generating PDFs
3. Extracting structured data
4. Automating browser interactions
It offers two main interfaces:
- **REST API**: Simple endpoints for common tasks
- **Workers Binding API**: Advanced integration with Puppeteer
The service runs within Cloudflare's network, providing low-latency access to browser capabilities without managing infrastructure.
`.trim();
// Truncate if necessary
const truncatedSummary = mockSummary.length > maxLength
? mockSummary.substring(0, maxLength) + '...'
: mockSummary;
return {
content: [
{
type: 'text',
text: truncatedSummary,
},
],
};
} catch (error) {
console.error('[Error] Error summarizing content:', error);
return {
content: [
{
type: 'text',
text: `Error summarizing content: ${error instanceof Error ? error.message : String(error)}`,
},
],
isError: true,
};
}
}
/**
* Handle the take_screenshot tool
*/
private async handleTakeScreenshot(args: any) {
// Validate arguments
if (typeof args !== 'object' || args === null || typeof args.url !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'Invalid arguments for take_screenshot');
}
const {
url,
width = 1280,
height = 800,
fullPage = false
} = args;
try {
console.error(`[API] Taking screenshot of ${url} with parameters:`, { width, height, fullPage });
console.error(`[API] Using endpoint: ${process.env.BROWSER_RENDERING_API}`);
// Take the screenshot - returns only the URL
const screenshotUrl = await this.browserClient.takeScreenshot(url, {
width,
height,
fullPage,
});
console.error('[API] Screenshot taken successfully, URL:', screenshotUrl);
// Return just the URL as text (without embedding the image)
return {
content: [
{
type: 'text',
text: `Screenshot of ${url} is available at: ${screenshotUrl}\n\n(URL provided as text to prevent potential rendering issues)`
}
]
};
} catch (error) {
console.error('[Error] Error taking screenshot:', error);
return {
content: [
{
type: 'text',
text: `Error taking screenshot: ${error instanceof Error ? error.message : String(error)}`,
}
],
isError: true,
};
}
}
/**
* Run the MCP server
*/
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
}
}