Jina.ai Grounding MCP Server
by spences10
Verified
- src
#!/usr/bin/env node
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');
}
interface GroundingReference {
url: string; // Source URL
keyQuote: string; // Supporting quote from the source
isSupportive: boolean; // Whether the reference supports or contradicts the statement
}
interface GroundingResponse {
factuality: number; // Score between 0-1 indicating confidence
result: boolean; // True if statement is verified as factual
reason: string; // Explanation of the verification result
references: GroundingReference[]; // Supporting/contradicting sources (up to 30)
usage: {
tokens: number; // Number of tokens processed
};
}
interface GroundingOptions {
statement: string; // Statement to verify
references?: string[]; // Optional list of URLs to restrict search to
no_cache?: boolean; // Whether to bypass cache for fresh results
}
const is_valid_grounding_args = (
args: any,
): args is GroundingOptions =>
typeof args === 'object' &&
args !== null &&
typeof args.statement === 'string' &&
args.statement.trim() !== '' &&
(args.references === undefined ||
(Array.isArray(args.references) &&
args.references.every((ref: string) => typeof ref === 'string'))) &&
(args.no_cache === undefined || typeof args.no_cache === 'boolean');
class JinaGroundingServer {
private server: Server;
private base_url = 'https://g.jina.ai';
constructor() {
this.server = new Server(
{
name,
version,
},
{
capabilities: {
tools: {},
},
},
);
this.setup_handlers();
this.server.onerror = (error) =>
console.error('[MCP Error]', error);
}
private 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: Record<string, string> = {
'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()) as {
code: number;
status: number;
data: GroundingResponse;
};
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);