server.smithery.jsā¢12.6 kB
#!/usr/bin/env node
const Database = require('better-sqlite3');
const path = require('path');
// Optimized MCP server with real database queries
class OptimizedMCPServer {
constructor() {
this.db = null;
this.tools = [
{
name: 'search_local',
description: 'Search the local EGW writings database (fast, limited results)',
inputSchema: {
type: 'object',
properties: {
query: { type: 'string', description: 'Search query' },
limit: { type: 'number', description: 'Maximum results (default: 10, max: 50)', default: 10 },
},
required: ['query'],
},
},
{
name: 'get_local_book',
description: 'Get information about a specific book',
inputSchema: {
type: 'object',
properties: {
bookId: { type: 'number', description: 'Book ID' },
},
required: ['bookId'],
},
},
{
name: 'get_local_content',
description: 'Get content from a specific book',
inputSchema: {
type: 'object',
properties: {
bookId: { type: 'number', description: 'Book ID' },
limit: { type: 'number', description: 'Maximum paragraphs (default: 10, max: 100)', default: 10 },
offset: { type: 'number', description: 'Offset for pagination (default: 0)', default: 0 },
},
required: ['bookId'],
},
},
{
name: 'list_local_books',
description: 'List all available books',
inputSchema: {
type: 'object',
properties: {
language: { type: 'string', description: 'Language filter (default: "en")', default: 'en' },
limit: { type: 'number', description: 'Maximum books (default: 20, max: 100)', default: 20 },
},
},
},
{
name: 'get_database_stats',
description: 'Get database statistics (fast summary)',
inputSchema: {
type: 'object',
properties: {},
},
},
];
}
initDatabase() {
if (this.db) return;
try {
const dbPath = path.join(__dirname, '..', 'data', 'egw-writings.db');
this.db = new Database(dbPath, {
readonly: true,
fileMustExist: true,
});
// Set pragmas for better performance
this.db.pragma('journal_mode = WAL');
this.db.pragma('synchronous = NORMAL');
this.db.pragma('temp_store = MEMORY');
this.db.pragma('mmap_size = 30000000000');
this.db.pragma('page_size = 4096');
this.db.pragma('cache_size = -64000'); // 64MB cache
// Prepare statements for reuse
this.prepareStatements();
process.stderr.write('Database initialized successfully\n');
} catch (error) {
process.stderr.write(`Database initialization error: ${error.message}\n`);
throw error;
}
}
prepareStatements() {
// Optimized prepared statements
this.stmts = {
search: this.db.prepare(`
SELECT
p.para_id,
p.book_id,
b.code as pub_code,
b.title as pub_name,
b.author,
b.pub_year,
substr(p.content, 1, 200) as snippet
FROM paragraphs p
JOIN books b ON p.book_id = b.book_id
WHERE p.content LIKE ? COLLATE NOCASE
LIMIT ?
`),
getBook: this.db.prepare(`
SELECT
book_id,
code,
lang,
type,
title,
author,
pub_year,
description
FROM books
WHERE book_id = ?
`),
getContent: this.db.prepare(`
SELECT
para_id,
content,
book_id
FROM paragraphs
WHERE book_id = ?
ORDER BY para_id
LIMIT ? OFFSET ?
`),
listBooks: this.db.prepare(`
SELECT
book_id,
code,
lang,
type,
title,
author,
pub_year
FROM books
WHERE lang = ?
ORDER BY title
LIMIT ?
`),
// Fast stats using cached counts
stats: this.db.prepare(`
SELECT
(SELECT COUNT(DISTINCT book_id) FROM books) as total_books,
(SELECT COUNT(DISTINCT book_id) FROM paragraphs) as books_with_content,
(SELECT COUNT(DISTINCT lang) FROM books) as languages
`),
// Estimate paragraph count (fast)
paragraphCount: this.db.prepare(`
SELECT COUNT(*) as count FROM paragraphs LIMIT 100000
`)
};
}
async handleRequest(request) {
try {
const { method, params, id } = request;
switch (method) {
case 'initialize':
this.initDatabase();
return {
jsonrpc: '2.0',
id,
result: {
protocolVersion: '2024-11-05',
capabilities: {
tools: {},
},
serverInfo: {
name: 'egw-research-server',
version: '1.0.0',
},
},
};
case 'tools/list':
return {
jsonrpc: '2.0',
id,
result: {
tools: this.tools,
},
};
case 'tools/call':
return await this.handleToolCall(params, id);
case 'notifications/initialized':
case 'notifications/cancelled':
return null;
default:
return {
jsonrpc: '2.0',
id,
error: {
code: -32601,
message: `Method not found: ${method}`,
},
};
}
} catch (error) {
process.stderr.write(`Request error: ${error.message}\n`);
return {
jsonrpc: '2.0',
id: request.id,
error: {
code: -32603,
message: `Internal error: ${error.message}`,
},
};
}
}
async handleToolCall(params, id) {
const { name, arguments: args } = params;
try {
if (!this.db) {
this.initDatabase();
}
switch (name) {
case 'search_local': {
const { query, limit = 10 } = args;
const safeLimit = Math.min(limit, 50); // Cap at 50 results
const startTime = Date.now();
const results = this.stmts.search.all(`%${query}%`, safeLimit);
const duration = Date.now() - startTime;
return {
jsonrpc: '2.0',
id,
result: {
content: [
{
type: 'text',
text: JSON.stringify({
results: results.map((r, idx) => ({
index: idx,
para_id: r.para_id,
book_id: r.book_id,
pub_code: r.pub_code,
pub_name: r.pub_name,
author: r.author,
pub_year: r.pub_year,
snippet: r.snippet + '...',
refcode_short: `${r.pub_code} ${r.para_id}`,
})),
total: results.length,
query: query,
query_time_ms: duration,
}, null, 2),
},
],
},
};
}
case 'get_local_book': {
const { bookId } = args;
const book = this.stmts.getBook.get(bookId);
if (!book) {
return {
jsonrpc: '2.0',
id,
error: {
code: -32602,
message: `Book not found: ${bookId}`,
},
};
}
return {
jsonrpc: '2.0',
id,
result: {
content: [
{
type: 'text',
text: JSON.stringify(book, null, 2),
},
],
},
};
}
case 'get_local_content': {
const { bookId, limit = 10, offset = 0 } = args;
const safeLimit = Math.min(limit, 100);
const paragraphs = this.stmts.getContent.all(bookId, safeLimit, offset);
return {
jsonrpc: '2.0',
id,
result: {
content: [
{
type: 'text',
text: JSON.stringify({
bookId: bookId,
paragraphs: paragraphs,
count: paragraphs.length,
offset: offset,
}, null, 2),
},
],
},
};
}
case 'list_local_books': {
const { language = 'en', limit = 20 } = args;
const safeLimit = Math.min(limit, 100);
const books = this.stmts.listBooks.all(language, safeLimit);
return {
jsonrpc: '2.0',
id,
result: {
content: [
{
type: 'text',
text: JSON.stringify({
books: books,
count: books.length,
language: language,
}, null, 2),
},
],
},
};
}
case 'get_database_stats': {
const startTime = Date.now();
// Get fast stats
const stats = this.stmts.stats.get();
// Get approximate paragraph count (with limit to prevent timeout)
let paragraphCount = 0;
try {
const result = this.stmts.paragraphCount.get();
paragraphCount = result.count;
} catch (error) {
process.stderr.write(`Paragraph count error: ${error.message}\n`);
paragraphCount = 'unavailable';
}
const duration = Date.now() - startTime;
return {
jsonrpc: '2.0',
id,
result: {
content: [
{
type: 'text',
text: JSON.stringify({
total_books: stats.total_books,
books_with_content: stats.books_with_content,
paragraphs: paragraphCount,
languages: stats.languages,
query_time_ms: duration,
note: paragraphCount === 'unavailable'
? 'Paragraph count unavailable - database too large'
: 'Stats retrieved successfully',
}, null, 2),
},
],
},
};
}
default:
return {
jsonrpc: '2.0',
id,
error: {
code: -32601,
message: `Unknown tool: ${name}`,
},
};
}
} catch (error) {
process.stderr.write(`Tool execution error: ${error.message}\n${error.stack}\n`);
return {
jsonrpc: '2.0',
id,
error: {
code: -32603,
message: `Tool execution error: ${error.message}`,
},
};
}
}
async run() {
process.stdin.setEncoding('utf8');
process.stdout.setEncoding('utf8');
let buffer = '';
process.stdin.on('data', async (chunk) => {
buffer += chunk;
const messages = buffer.split('\n');
buffer = messages.pop() || '';
for (const message of messages) {
if (message.trim()) {
try {
const request = JSON.parse(message);
const response = await this.handleRequest(request);
if (response) {
process.stdout.write(JSON.stringify(response) + '\n');
}
} catch (error) {
process.stderr.write(`Error processing request: ${error.message}\n`);
}
}
}
});
process.stdin.on('end', () => {
if (this.db) {
this.db.close();
}
process.exit(0);
});
process.on('SIGINT', () => {
if (this.db) {
this.db.close();
}
process.exit(0);
});
process.on('SIGTERM', () => {
if (this.db) {
this.db.close();
}
process.exit(0);
});
}
}
// Start the server
const server = new OptimizedMCPServer();
server.run().catch(error => {
process.stderr.write(`Server error: ${error.message}\n`);
process.exit(1);
});