Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
84 lines
2.6 KiB
JavaScript
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);
|
|
});
|