import express from 'express';
import cors from 'cors';
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
Tool
} from '@modelcontextprotocol/sdk/types.js';
import { DanbooruService } from './services/danbooru.js';
import { TursoService } from './services/turso.js';
import { collectAndSave } from './tools/collect.js';
// Environment variables
const PORT = process.env.PORT || 3000;
const TURSO_DATABASE_URL = process.env.TURSO_DATABASE_URL;
const TURSO_AUTH_TOKEN = process.env.TURSO_AUTH_TOKEN;
if (!TURSO_DATABASE_URL || !TURSO_AUTH_TOKEN) {
console.error('Error: TURSO_DATABASE_URL and TURSO_AUTH_TOKEN required');
process.exit(1);
}
// Initialize services
const danbooru = new DanbooruService();
const turso = new TursoService(TURSO_DATABASE_URL, TURSO_AUTH_TOKEN);
// Initialize database
await turso.initialize();
// Define tools (only collect_and_save)
const tools: Tool[] = [
{
name: 'collect_and_save',
description: `**[DATA COLLECTION]** Collect character posts from Danbooru API and save to Turso database with automatic pagination and upserts (updates existing posts).
**IMPORTANT: Before collecting, check post count first!**
1. Use danbooru-tags-mcp.get_post_count(tag) to see total posts and get collection strategy
2. If get_post_count is unavailable, scrape Danbooru website for post count
3. Plan your collection batches using start_page parameter based on the count
Use this when:
- You want to BUILD a dataset for later analysis
- You need to save posts for offline or repeated queries
- You want to collect large amounts of data (100+ posts) efficiently
- You need to keep the database updated with new posts
For large datasets (5000+ posts):
- Use start_page parameter to collect in batches
- Each page = 200 posts, so start_page=11 begins at post 2001
- Example: 9000 posts = 5 batches (pages 1, 11, 21, 31, 41)
After collection, use Turso SQL tools to query and analyze the saved data.
For real-time exploration without saving, use Danbooru Tags tools.`,
inputSchema: {
type: 'object',
properties: {
tag: {
type: 'string',
description: 'Character tag to collect (e.g., "yatogami_tenka")'
},
max_posts: {
type: 'number',
description: 'Maximum number of posts to collect (optional)'
},
start_page: {
type: 'number',
description: 'Starting page number for pagination (default: 1, each page has 200 posts)'
}
},
required: ['tag']
}
}
];
// Create MCP server
const server = new Server(
{
name: 'danbooru-turso-mcp',
version: '1.0.0'
},
{
capabilities: {
tools: {}
}
}
);
// Tool handlers
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
if (name === 'collect_and_save') {
const { tag, max_posts, start_page } = args as { tag: string; max_posts?: number; start_page?: number };
const result = await collectAndSave(danbooru, turso, tag, max_posts, start_page);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
}
throw new Error(`Unknown tool: ${name}`);
} catch (error) {
return {
content: [{
type: 'text',
text: JSON.stringify({
error: error instanceof Error ? error.message : String(error)
})
}],
isError: true
};
}
});
// HTTP Server for Smithery
const app = express();
app.use(cors({ origin: '*' }));
app.use(express.json());
// Helper function to send response in JSON or SSE format
function sendResponse(res: any, acceptHeader: string, data: any) {
const wantsSSE = acceptHeader.includes('text/event-stream');
if (wantsSSE) {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.write('event: message\n');
res.write(`data: ${JSON.stringify(data)}\n\n`);
res.end();
} else {
res.json(data);
}
}
// Health check
app.get('/health', (req, res) => {
res.json({ status: 'healthy' });
});
// Server info
app.get('/mcp', (req, res) => {
res.json({
name: 'danbooru-turso-mcp',
version: '1.0.0',
description: 'Collect Danbooru data and save to Turso database',
endpoints: {
mcp: 'POST /mcp - JSON-RPC endpoint',
sse: 'GET /sse - Server-Sent Events endpoint',
health: 'GET /health - Health check'
},
tools: tools.map(t => ({ name: t.name, description: t.description }))
});
});
// Main MCP endpoint
app.post('/mcp', async (req, res) => {
const acceptHeader = req.headers['accept'] || '';
const request = req.body;
try {
// Handle initialize
if (request.method === 'initialize') {
sendResponse(res, acceptHeader, {
jsonrpc: '2.0',
id: request.id,
result: {
protocolVersion: '2024-11-05',
capabilities: { tools: {} },
serverInfo: { name: 'danbooru-turso-mcp', version: '1.0.0' }
}
});
return;
}
// Handle tools/list
if (request.method === 'tools/list') {
sendResponse(res, acceptHeader, {
jsonrpc: '2.0',
id: request.id,
result: { tools }
});
return;
}
// Handle tools/call
if (request.method === 'tools/call') {
const { name, arguments: args } = request.params;
try {
if (name === 'collect_and_save') {
const { tag, max_posts, start_page } = args as { tag: string; max_posts?: number; start_page?: number };
const toolResult = await collectAndSave(danbooru, turso, tag, max_posts, start_page);
sendResponse(res, acceptHeader, {
jsonrpc: '2.0',
id: request.id,
result: {
content: [{ type: 'text', text: JSON.stringify(toolResult, null, 2) }]
}
});
} else {
sendResponse(res, acceptHeader, {
jsonrpc: '2.0',
id: request.id,
error: { code: -32601, message: `Unknown tool: ${name}` }
});
}
} catch (toolError) {
sendResponse(res, acceptHeader, {
jsonrpc: '2.0',
id: request.id,
result: {
content: [{
type: 'text',
text: JSON.stringify({
error: toolError instanceof Error ? toolError.message : String(toolError)
})
}],
isError: true
}
});
}
return;
}
// Handle empty capabilities
if (request.method === 'resources/list') {
sendResponse(res, acceptHeader, {
jsonrpc: '2.0',
id: request.id,
result: { resources: [] }
});
return;
}
if (request.method === 'prompts/list') {
sendResponse(res, acceptHeader, {
jsonrpc: '2.0',
id: request.id,
result: { prompts: [] }
});
return;
}
if (request.method === 'resources/templates/list') {
sendResponse(res, acceptHeader, {
jsonrpc: '2.0',
id: request.id,
result: { resourceTemplates: [] }
});
return;
}
// Handle ping/heartbeat
if (request.method === 'ping' || request.method === 'heartbeat') {
sendResponse(res, acceptHeader, {
jsonrpc: '2.0',
id: request.id,
result: {}
});
return;
}
// Handle notifications
if (request.method?.startsWith('notifications/')) {
console.log(`Notification received: ${request.method}`);
if (acceptHeader.includes('text/event-stream')) {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.write('\n\n');
res.end();
} else {
res.status(200).end();
}
return;
}
// Unknown method
sendResponse(res, acceptHeader, {
jsonrpc: '2.0',
id: request.id,
error: { code: -32601, message: 'Method not found' }
});
} catch (error) {
console.error('Request error:', error);
sendResponse(res, acceptHeader, {
jsonrpc: '2.0',
id: request.id,
error: {
code: -32603,
message: error instanceof Error ? error.message : 'Internal error'
}
});
}
});
// SSE endpoint for MCP
app.get('/sse', async (req, res) => {
console.error('SSE connection established');
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
const transport = new SSEServerTransport('/message', res);
await server.connect(transport);
req.on('close', () => {
console.error('SSE connection closed');
});
});
app.post('/message', async (req, res) => {
console.error('Message received:', req.body);
res.status(200).json({ received: true });
});
// Start HTTP server
const server_instance = app.listen(Number(PORT), '0.0.0.0', () => {
console.error(`Danbooru-Turso MCP server running on port ${PORT}`);
console.error(`SSE endpoint: http://0.0.0.0:${PORT}/sse`);
console.error(`Health check: http://0.0.0.0:${PORT}/health`);
console.error(`Database: ${TURSO_DATABASE_URL}`);
});
// Graceful shutdown
process.on('SIGTERM', () => {
console.error('SIGTERM signal received: closing HTTP server');
server_instance.close(() => {
console.error('HTTP server closed');
process.exit(0);
});
});
process.on('SIGINT', () => {
console.error('SIGINT signal received: closing HTTP server');
server_instance.close(() => {
console.error('HTTP server closed');
process.exit(0);
});
});