@ildunari/jina-mcp-advanced
Version:
Pure MCP Server for Jina.AI Advanced web scraping
274 lines (251 loc) • 7.64 kB
JavaScript
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import fetch from 'node-fetch';
// Get API key from environment variable
const jinaApiKey = process.env.JINA_API_KEY;
if (!jinaApiKey) {
console.error('Error: JINA_API_KEY environment variable is required.');
process.exit(1);
}
// Jina Reader Advanced function
async function jina_reader_advanced(params) {
const {
url,
return_format = 'markdown',
engine = 'default',
retain_images = false,
json_response = false,
summarize_links = true,
summarize_images = false,
image_caption = false,
use_readerlm = false,
bypass_cache = false,
target_selector,
wait_for_selector,
exclude_selector,
timeout
} = params;
if (!url) {
throw new Error('URL is required');
}
try {
const headers = {
'Authorization': `Bearer ${jinaApiKey}`
};
if (engine !== 'default') {
headers['X-Engine'] = engine;
}
if (return_format !== 'default') {
headers['X-Return-Format'] = return_format;
}
headers['X-Retain-Images'] = retain_images ? 'true' : 'none';
if (json_response) {
headers['Accept'] = 'application/json';
}
if (!summarize_links) {
headers['X-With-Links-Summary'] = 'false';
} else {
headers['X-With-Links-Summary'] = 'true';
}
if (summarize_images) {
headers['X-With-Images-Summary'] = 'true';
}
if (image_caption) {
headers['X-Image-Caption'] = 'true';
}
if (use_readerlm) {
headers['X-Reader-LM'] = 'true';
}
if (bypass_cache) {
headers['X-No-Cache'] = 'true';
}
if (target_selector) {
headers['X-Target-Selector'] = target_selector;
}
if (wait_for_selector) {
headers['X-Wait-For-Selector'] = wait_for_selector;
}
if (exclude_selector) {
headers['X-Exclude-Selector'] = exclude_selector;
}
if (timeout && Number.isInteger(timeout) && timeout > 0) {
headers['X-Timeout'] = timeout.toString();
}
const apiUrl = `https://r.jina.ai/${encodeURIComponent(url)}`;
const response = await fetch(apiUrl, {
method: 'GET',
headers: headers
});
if (!response.ok) {
let errorBody = await response.text();
try {
errorBody = JSON.stringify(JSON.parse(errorBody), null, 2);
} catch(e) { /* Ignore if response body is not JSON */ }
throw new Error(`Failed to fetch content (${response.status} ${response.statusText}): ${errorBody}`);
}
const contentType = response.headers.get('content-type') || '';
let content;
if (json_response || contentType.includes('application/json')) {
content = await response.json();
if (!json_response && typeof content === 'object' && content !== null) {
return { response_type: 'json_received_unexpectedly', data: content };
}
} else if (contentType.includes('text/')) {
content = await response.text();
} else if (contentType.includes('image/')) {
const arrayBuffer = await response.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
content = `data:${contentType};base64,${buffer.toString('base64')}`;
return { response_type: 'image_data_url', format: return_format, data_url: content };
} else {
return { response_type: 'blob', content_type: contentType, message: "Received unexpected binary data, cannot display directly." };
}
return content;
} catch (error) {
throw error;
}
}
// Create the Jina tool definition
const JINA_TOOL = {
name: 'read_webpage',
description: 'Fetch and extract content from a URL using Jina Reader Advanced. Supports multiple output formats and advanced scraping options.',
inputSchema: {
type: 'object',
properties: {
url: {
type: 'string',
description: 'The URL to fetch content from',
},
return_format: {
type: 'string',
enum: ['default', 'markdown', 'html', 'text', 'screenshot', 'pageshot'],
default: 'markdown',
description: 'The desired format for the scraped content. markdown recommended for readability.',
},
engine: {
type: 'string',
enum: ['default', 'browser'],
default: 'default',
description: 'The rendering engine to use. Use "browser" for JavaScript-heavy sites.',
},
json_response: {
type: 'boolean',
default: false,
description: 'Whether to return structured JSON response',
},
retain_images: {
type: 'boolean',
default: false,
description: 'Whether to retain images in the output',
},
summarize_links: {
type: 'boolean',
default: true,
description: 'Whether to include link summaries',
},
summarize_images: {
type: 'boolean',
default: false,
description: 'Whether to include image summaries',
},
image_caption: {
type: 'boolean',
default: false,
description: 'Whether to generate image captions',
},
use_readerlm: {
type: 'boolean',
default: false,
description: 'Whether to use ReaderLM for enhanced content understanding',
},
bypass_cache: {
type: 'boolean',
default: false,
description: 'Whether to bypass cache and fetch fresh content',
},
target_selector: {
type: 'string',
description: 'CSS selector to target specific content on the page',
},
wait_for_selector: {
type: 'string',
description: 'Wait for this CSS selector to appear before scraping',
},
exclude_selector: {
type: 'string',
description: 'CSS selector for elements to exclude from scraping',
},
timeout: {
type: 'integer',
description: 'Request timeout in seconds',
},
},
required: ['url'],
},
};
// Create MCP server
const server = new Server(
{
name: 'jina-mcp-advanced',
version: '1.3.0',
},
{
capabilities: {
tools: {},
},
}
);
// Handle list tools request
server.setRequestHandler(ListToolsRequestSchema, async (request) => {
return {
tools: [JINA_TOOL],
};
});
// Handle call tool request
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === 'read_webpage') {
try {
const content = await jina_reader_advanced(request.params.arguments);
return {
content: [
{
type: 'text',
text: typeof content === 'string' ? content : JSON.stringify(content, null, 2),
},
],
};
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error: ${error.message}`,
},
],
isError: true,
};
}
}
return {
content: [
{
type: 'text',
text: `Unknown tool: ${request.params.name}`,
},
],
isError: true,
};
});
// Start the server
async function runServer() {
const transport = new StdioServerTransport();
await server.connect(transport);
// Log to stderr so it doesn't interfere with MCP JSON-RPC protocol on stdout
console.error('Jina MCP Server started successfully');
}
runServer().catch((error) => {
console.error('Fatal error running server:', error);
process.exit(1);
});