Files
mosaic/packages/brain/src/index.ts
2026-03-11 01:12:47 +00:00

84 lines
2.6 KiB
JavaScript

#!/usr/bin/env node
import Fastify from 'fastify';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { JsonStore } from './storage/json-store.js';
import { Collections } from './storage/collections.js';
import { requireAuth } from './middleware/auth.js';
import { registerRoutes } from './routes/api.js';
import { registerMcpTools } from './mcp/tools.js';
const HOST = process.env['HOST'] ?? '0.0.0.0';
const PORT = Number(process.env['PORT'] ?? 8100);
const DATA_DIR = process.env['MOSAIC_BRAIN_DATA_DIR'] ?? './data';
/**
* Create a fresh MCP server instance with all tools registered.
* Each HTTP request gets its own instance to avoid state leaks.
*/
function createMcpServer(collections: Collections): McpServer {
const mcp = new McpServer({
name: 'mosaic-brain',
version: '0.1.0',
});
registerMcpTools(mcp, collections);
return mcp;
}
async function main(): Promise<void> {
// --- Storage ---
const store = new JsonStore({ dataDir: DATA_DIR });
await store.init();
const collections = new Collections(store);
// --- Fastify ---
const app = Fastify({ logger: true });
// Auth on all /v1 and /mcp routes
app.addHook('onRequest', async (req, reply) => {
if (req.url.startsWith('/v1/') || req.url === '/mcp') {
await requireAuth(req, reply);
}
});
// REST API
registerRoutes(app, collections);
// MCP over HTTP (Streamable HTTP transport)
// Each request gets a fresh McpServer + transport to avoid state leaks
app.all('/mcp', async (req, reply) => {
const mcp = createMcpServer(collections);
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined, // stateless
});
await mcp.connect(transport);
if (req.method === 'POST') {
await transport.handleRequest(
req.raw,
reply.raw,
req.body as Record<string, unknown>,
);
} else if (req.method === 'GET') {
await transport.handleRequest(req.raw, reply.raw);
} else if (req.method === 'DELETE') {
await transport.handleRequest(req.raw, reply.raw);
} else {
reply.code(405).send({ error: 'Method not allowed' });
}
});
// --- Start ---
await app.listen({ host: HOST, port: PORT });
console.log(`mosaic-brain listening on ${HOST}:${PORT}`);
console.log(` REST API: http://${HOST}:${PORT}/v1/`);
console.log(` MCP: http://${HOST}:${PORT}/mcp`);
console.log(` Data dir: ${DATA_DIR}`);
}
main().catch((err) => {
console.error('Failed to start mosaic-brain:', err);
process.exit(1);
});