mcp-jinaai-grounding
Version:
MCP server for JinaAI grounding
155 lines (154 loc) • 7.62 kB
JavaScript
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 { readFileSync } from 'fs';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
const { name, version } = pkg;
// Get your Jina AI API key for free: https://jina.ai/?sui=apikey
const JINAAI_API_KEY = process.env.JINAAI_API_KEY;
if (!JINAAI_API_KEY) {
throw new Error('JINAAI_API_KEY environment variable is required');
}
const is_valid_grounding_args = (args) => typeof args === 'object' &&
args !== null &&
typeof args.statement === 'string' &&
args.statement.trim() !== '' &&
(args.references === undefined ||
(Array.isArray(args.references) &&
args.references.every((ref) => typeof ref === 'string'))) &&
(args.no_cache === undefined || typeof args.no_cache === 'boolean');
class JinaGroundingServer {
constructor() {
this.base_url = 'https://g.jina.ai';
this.server = new Server({
name,
version,
}, {
capabilities: {
tools: {},
},
});
this.setup_handlers();
this.server.onerror = (error) => console.error('[MCP Error]', error);
}
setup_handlers() {
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'ground_statement',
description: 'Ground a statement using real-time web search results to check factuality. ' +
'When providing URLs via the references parameter, ensure they are publicly accessible ' +
'and contain relevant information about the statement. If the URLs do not contain ' +
'the necessary information, try removing the URL restrictions to search the entire web.',
inputSchema: {
type: 'object',
properties: {
statement: {
type: 'string',
description: 'Statement to be grounded',
},
references: {
type: 'array',
items: {
type: 'string',
},
description: 'Optional list of URLs to restrict search to. Only provide URLs that are ' +
'publicly accessible and contain information relevant to the statement. ' +
'If the URLs do not contain the necessary information, the grounding will fail. ' +
'For best results, either provide URLs you are certain contain the information, ' +
'or omit this parameter to search the entire web.',
},
no_cache: {
type: 'boolean',
description: 'Whether to bypass cache for fresh results',
default: false,
},
},
required: ['statement'],
},
},
],
}));
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name !== 'ground_statement') {
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
}
const args = request.params.arguments;
if (!is_valid_grounding_args(args)) {
throw new McpError(ErrorCode.InvalidParams, 'Invalid parameters. Required: statement (string). Optional: references (string[]), no_cache (boolean)');
}
try {
const headers = {
'Content-Type': 'application/json',
Accept: 'application/json',
Authorization: `Bearer ${JINAAI_API_KEY}`,
};
// Add optional headers
if (args.references?.length) {
headers['X-Site'] = args.references.join(',');
}
if (args.no_cache) {
headers['X-No-Cache'] = 'true';
}
const response = await fetch(this.base_url, {
method: 'POST',
headers,
body: JSON.stringify({
statement: args.statement.trim(),
}),
});
if (!response.ok) {
const error_text = await response.text();
let error_json;
try {
error_json = JSON.parse(error_text);
}
catch {
throw new Error(`HTTP error! status: ${response.status}, message: ${error_text}`);
}
// Handle specific error cases
if (error_json.status === 42206) {
throw new McpError(ErrorCode.InvalidParams, 'The provided URLs did not contain relevant information for fact-checking. This can happen when:\n' +
'1. The URLs are not publicly accessible\n' +
'2. The URLs do not contain information about the specific statement\n' +
'3. The information exists but is not in an easily searchable format\n\n' +
'Suggestions:\n' +
'- Remove the URL restrictions to search the entire web\n' +
'- Provide different URLs that you are certain contain the information\n' +
'- Verify the URLs are publicly accessible and contain relevant content');
}
// Handle other API errors
throw new Error(`API error! status: ${response.status}, message: ${error_json.readableMessage || error_json.message || error_text}`);
}
const result = (await response.json());
if (result.code !== 200) {
throw new Error(`API error! status: ${result.status}`);
}
return {
content: [
{
type: 'text',
text: JSON.stringify(result.data, null, 2),
},
],
};
}
catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new McpError(ErrorCode.InternalError, `Failed to ground statement: ${message}`);
}
});
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('Jina Grounding MCP server running on stdio');
}
}
const server = new JinaGroundingServer();
server.run().catch(console.error);