feat(brain): @mosaic/brain structured data service (#11)
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #11.
This commit is contained in:
83
packages/brain/src/index.ts
Normal file
83
packages/brain/src/index.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
#!/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);
|
||||
});
|
||||
Reference in New Issue
Block a user