twenty-mcp-server
Version:
Easy-to-install Model Context Protocol server for Twenty CRM. Try instantly with 'npx twenty-mcp-server setup' or install globally for permanent use.
237 lines • 10.1 kB
JavaScript
import { createServer } from 'node:http';
import { randomUUID } from 'node:crypto';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { TwentyClient } from './client/twenty-client.js';
import { registerPersonTools, registerCompanyTools, registerTaskTools, registerOpportunityTools } from './tools/index.js';
import { WellKnownRoutes } from './routes/well-known.js';
import { AuthMiddleware } from './auth/middleware.js';
import { TokenValidator } from './auth/token-validator.js';
import { ClerkClient } from './auth/clerk-client.js';
import { getKeyStorageService } from './auth/key-storage.js';
import { ApiKeyRoutes } from './routes/api-keys.js';
import { IPMiddleware } from './auth/ip-middleware.js';
async function main() {
const port = parseInt(process.env.PORT || '3000');
const authEnabled = process.env.AUTH_ENABLED === 'true';
const wellKnownRoutes = new WellKnownRoutes();
const ipMiddleware = new IPMiddleware();
// Initialize auth components only if auth is enabled
let clerkClient = null;
let tokenValidator = null;
let authMiddleware = null;
let keyStorage = null;
let apiKeyRoutes = null;
if (authEnabled) {
clerkClient = new ClerkClient();
tokenValidator = new TokenValidator(clerkClient);
authMiddleware = new AuthMiddleware(tokenValidator);
keyStorage = getKeyStorageService();
apiKeyRoutes = new ApiKeyRoutes();
}
// Parse configuration from multiple sources
async function parseConfig(url, userId) {
const urlObj = new URL(url, `http://localhost:${port}`);
const params = urlObj.searchParams;
// Check for user-specific stored API key first
let apiKey = params.get('apiKey');
let baseUrl = params.get('baseUrl');
if (authEnabled && userId && !apiKey && keyStorage) {
const storedKey = await keyStorage.getApiKey(userId);
if (storedKey) {
apiKey = storedKey.twentyApiKey;
baseUrl = storedKey.twentyBaseUrl || baseUrl;
}
}
// Priority: URL params > User stored key > Environment variables > Smithery config
return {
apiKey: apiKey ||
process.env.TWENTY_API_KEY ||
process.env.SMITHERY_CONFIG_APIKEY ||
process.env.apiKey,
baseUrl: baseUrl ||
process.env.TWENTY_BASE_URL ||
process.env.SMITHERY_CONFIG_BASEURL ||
process.env.baseUrl ||
'https://api.twenty.com',
};
}
// Create HTTP server
const httpServer = createServer(async (req, res) => {
// Check IP allowlist first (before any other processing)
if (!await ipMiddleware.checkAccess(req, res)) {
return; // IP middleware already sent response
}
// Handle CORS preflight requests
if (req.method === 'OPTIONS') {
await wellKnownRoutes.handleOptions(req, res);
return;
}
// Handle health check endpoint
if (req.url === '/health') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
status: 'healthy',
service: 'twenty-mcp-server',
authEnabled,
ipProtection: ipMiddleware.getConfig().enabled
}));
return;
}
// Handle API key management endpoints
if (req.url?.startsWith('/api/keys')) {
if (!authEnabled || !authMiddleware || !apiKeyRoutes) {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('Not Found');
return;
}
const authReq = req;
if (!await authMiddleware.authenticate(authReq, res)) {
return;
}
await apiKeyRoutes.handle(authReq, res);
return;
}
// Handle OAuth discovery endpoints
if (req.url === '/.well-known/oauth-protected-resource') {
if (!authEnabled) {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('Not Found');
return;
}
await wellKnownRoutes.handleProtectedResource(req, res);
return;
}
if (req.url === '/.well-known/oauth-authorization-server') {
if (!authEnabled) {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('Not Found');
return;
}
await wellKnownRoutes.handleAuthorizationServer(req, res);
return;
}
// Only handle /mcp endpoint
if (!req.url?.startsWith('/mcp')) {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('Not Found');
return;
}
try {
// Authenticate request if auth is enabled
const authReq = req;
if (authEnabled && authMiddleware) {
if (!await authMiddleware.authenticate(authReq, res)) {
return; // Auth middleware already sent response
}
}
// Parse configuration from query parameters
const userId = authReq.auth?.userId;
const config = await parseConfig(req.url, userId);
if (!config.apiKey) {
// If authenticated but no API key stored
if (authEnabled && userId) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
error: 'No API key configured',
error_description: 'Please configure your Twenty API key first'
}));
return;
}
// For non-authenticated requests
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
error: 'Missing required apiKey parameter'
}));
return;
}
// Create MCP server with Twenty client
const server = new McpServer({
name: 'twenty-mcp-server',
version: '1.0.0',
}, {
capabilities: {
tools: {},
experimental: {
authentication: {
type: 'oauth2',
required: authEnabled && process.env.REQUIRE_AUTH === 'true',
enabled: authEnabled,
discoveryEndpoints: authEnabled ? {
protectedResource: '/.well-known/oauth-protected-resource',
authorizationServer: '/.well-known/oauth-authorization-server'
} : undefined
}
}
}
});
const client = new TwentyClient({
apiKey: config.apiKey,
baseUrl: config.baseUrl,
});
// Register tools
registerPersonTools(server, client);
registerCompanyTools(server, client);
registerTaskTools(server, client);
registerOpportunityTools(server, client);
// Create streamable HTTP transport
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
});
// Connect server to transport
await server.connect(transport);
// Parse request body for POST requests
let body = undefined;
if (req.method === 'POST') {
const chunks = [];
req.on('data', (chunk) => chunks.push(chunk));
req.on('end', async () => {
try {
const bodyText = Buffer.concat(chunks).toString();
if (bodyText.trim()) {
body = JSON.parse(bodyText);
}
await transport.handleRequest(req, res, body);
}
catch (error) {
console.error('Error parsing request body:', error);
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Invalid JSON in request body' }));
}
});
}
else {
// Handle GET/DELETE requests
await transport.handleRequest(req, res, body);
}
}
catch (error) {
console.error('Error handling request:', error);
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
error: 'Internal server error',
message: error instanceof Error ? error.message : 'Unknown error'
}));
}
});
httpServer.listen(port, () => {
console.log(`Twenty MCP Server running at http://localhost:${port}/mcp`);
console.log(`Health check available at http://localhost:${port}/health`);
// Log configuration source for debugging
if (process.env.SMITHERY_CONFIG_APIKEY) {
console.log('Running in Smithery environment');
}
else if (process.env.TWENTY_API_KEY) {
console.log('Using environment variables for configuration');
}
else {
console.log(`Example: http://localhost:${port}/mcp?apiKey=YOUR_API_KEY&baseUrl=https://api.twenty.com`);
}
});
}
main().catch((error) => {
console.error('Server error:', error);
process.exit(1);
});
//# sourceMappingURL=http-server.js.map