#!/usr/bin/env node
/**
* Self-hosted MCP server for kluster.ai Verify
*
* Provides fact-checking and verification tools using kluster.ai's API.
* Implements MCP protocol over HTTP Streamable transport.
*
* @author kluster.ai
* @version 1.0.0
*/
import { Command } from 'commander';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import fastify from 'fastify';
import { z } from 'zod';
import { KlusterAIClient } from './klusterClient.js';
import type { MCPVerificationResult } from './types.js';
// CLI configuration
const program = new Command();
program
.name('kluster-verify-mcp')
.description('Self-hosted MCP server for kluster.ai Verify')
.option('--api-key <key>', 'kluster.ai API key')
.option('--base-url <url>', 'kluster.ai base URL', 'https://api.kluster.ai/v1')
.option('--port <port>', 'Server port', '3001')
.parse();
const options = program.opts();
// Validate required configuration
const apiKey = options.apiKey || process.env.KLUSTER_API_KEY;
if (!apiKey) {
console.error('Error: API key is required. Use --api-key or KLUSTER_API_KEY environment variable.');
process.exit(1);
}
const baseUrl = options.baseUrl || process.env.KLUSTER_BASE_URL || 'https://api.kluster.ai/v1';
const port = parseInt(options.port) || 3001;
/**
* Creates an MCP server instance with kluster.ai Verify tools
*/
function createMcpServer(serverApiKey?: string): McpServer {
const effectiveApiKey = serverApiKey || apiKey;
const mcpServer = new McpServer(
{
name: 'kluster-verify-mcp-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
logging: {}
}
}
);
// Tool 1: verify
mcpServer.tool(
'verify',
'Fact-check a prompt from a user and response from the agent against reliable sources using kluster.ai',
{
prompt: z.string().describe('The prompt the user made to the agent. If there are multiple prompts please do multiple calls.'),
response: z.string().describe('The response from the agent that must be verified. If there are multiple responses for the same prompt please combine them into a single response. This parameter should never be empty.'),
returnSearchResults: z
.boolean()
.describe('Whether to return search results for verification')
.default(true),
},
async ({ prompt, response, returnSearchResults }) => {
try {
// Create client with effective API key
const client = new KlusterAIClient(effectiveApiKey, baseUrl);
// Call kluster.ai API
const result = await client.verifyClaim(
prompt,
response,
undefined,
returnSearchResults
);
// Transform response to match expected format
const mcpResult: MCPVerificationResult = {
prompt,
response,
is_hallucination: result.is_hallucination,
explanation: result.explanation,
confidence: result.usage,
search_results: result.search_results || [],
};
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(mcpResult, null, 2),
},
],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return {
content: [
{
type: 'text' as const,
text: `Error: ${errorMessage}`,
},
],
};
}
}
);
// Tool 2: verify_document
mcpServer.tool(
'verify_document',
'Verify if a response from the agent accurately reflects the content of a source document based on the user\'s prompt',
{
prompt: z
.string()
.describe('The prompt the user made to the agent about the document. If there are multiple prompts please do multiple calls.'),
response: z
.string()
.describe('The response from the agent that must be verified against the document content. If there are multiple responses for the same prompt please combine them into a single response. This parameter should never be empty.'),
documentContent: z
.string()
.describe('The full text content of the source document that the claim is about'),
returnSearchResults: z
.boolean()
.describe('Whether to return additional search results for cross-verification')
.default(true),
},
async ({ prompt, response, documentContent, returnSearchResults }) => {
try {
// Create client with effective API key
const client = new KlusterAIClient(effectiveApiKey, baseUrl);
// Call kluster.ai API with document context
const result = await client.verifyClaim(
prompt,
response,
documentContent,
returnSearchResults
);
// Transform response to match expected format
const mcpResult: MCPVerificationResult = {
prompt,
response,
is_hallucination: result.is_hallucination,
explanation: result.explanation,
confidence: result.usage,
search_results: result.search_results || [],
};
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(mcpResult, null, 2),
},
],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return {
content: [
{
type: 'text' as const,
text: `Error: ${errorMessage}`,
},
],
};
}
}
);
// Add information resource
mcpServer.resource(
'info',
'info://server',
{ mimeType: 'text/plain' },
async () => {
return {
contents: [
{
uri: 'info://server',
text: 'This is a self-hosted kluster.ai MCP server providing fact-checking and verification tools using kluster.ai technology.',
},
],
};
}
);
return mcpServer;
}
/**
* Creates a Fastify server with MCP endpoints
*
* @returns Configured Fastify application with MCP endpoints
*/
async function createFastifyServer() {
const app = fastify({ logger: false });
// Handle MCP requests at /stream endpoint
app.post('/stream', async (request, reply) => {
// Extract API key from header (like cloud server)
const headerApiKey = request.headers['x-api-key'] as string;
const finalApiKey = headerApiKey || apiKey;
if (!finalApiKey) {
reply.code(401).send({
jsonrpc: '2.0',
error: { code: -32001, message: 'API key required' },
id: null
});
return;
}
const server = createMcpServer(finalApiKey);
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
// Handle connection cleanup
reply.raw.on('close', async () => {
await transport.close();
await server.server.close();
});
// Connect server to transport and handle request
await server.server.connect(transport);
await transport.handleRequest(request.raw, reply.raw, request.body);
});
// Health check endpoint
app.get('/health', async () => {
return {
status: 'ok',
server: 'kluster-verify-mcp-server',
version: '1.0.0',
port,
endpoint: '/stream',
timestamp: new Date().toISOString(),
};
});
return app;
}
/**
* Initialize and start the MCP server
*
* Configures Fastify server, connects MCP transport, and starts listening
* for incoming connections on the specified port.
*/
async function main() {
try {
const app = await createFastifyServer();
await app.listen({ port, host: '0.0.0.0' });
console.log(`kluster.ai Verify MCP Server started on port ${port}`);
console.log(`MCP Endpoint: http://localhost:${port}/stream`);
} catch (error) {
console.error('Failed to start server:', error);
process.exit(1);
}
}
// Handle graceful shutdown
process.on('SIGINT', () => {
console.log('Shutting down gracefully...');
process.exit(0);
});
process.on('SIGTERM', () => {
console.log('Shutting down gracefully...');
process.exit(0);
});
// Start the server
main().catch((error) => {
console.error('Server error:', error);
process.exit(1);
});