Tesla MCP Server

  • src
/** * Tesla API Registration Server * * This server handles the Tesla API registration process: * 1. Hosts the public key at /.well-known/appspecific/com.tesla.3p.public-key.pem * 2. Registers the application with the Tesla API */ import express, { Request, Response } from 'express'; import axios from 'axios'; import dotenv from 'dotenv'; import path from 'path'; import { fileURLToPath } from 'url'; import { dirname } from 'path'; import fs from 'fs'; import * as crypto from 'crypto'; import { exec } from 'child_process'; import ngrok from 'ngrok'; // For ESM __dirname equivalent const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); // Load environment variables dotenv.config(); // Configuration const PORT = 4000; // Use a different port than the MCP server const KEYS_DIR = path.join(__dirname, '../keys'); const PRIVATE_KEY_PATH = path.join(KEYS_DIR, 'private-key.pem'); const PUBLIC_KEY_PATH = path.join(KEYS_DIR, 'public-key.pem'); const PUBLIC_KEY_ENDPOINT = '/.well-known/appspecific/com.tesla.3p.public-key.pem'; // Tesla API configuration const BASE_URL = 'https://fleet-api.prd.na.vn.cloud.tesla.com'; // Change if needed for your region const app = express(); // Ensure keys directory exists if (!fs.existsSync(KEYS_DIR)) { fs.mkdirSync(KEYS_DIR, { recursive: true }); } /** * Generate EC key pair if it doesn't exist */ async function generateKeyPair(): Promise<void> { if (fs.existsSync(PRIVATE_KEY_PATH) && fs.existsSync(PUBLIC_KEY_PATH)) { console.log('Key pair already exists'); return; } console.log('Generating EC key pair...'); return new Promise<void>((resolve, reject) => { exec(`openssl ecparam -name prime256v1 -genkey -noout -out ${PRIVATE_KEY_PATH}`, (error, stdout, stderr) => { if (error) { console.error(`Error generating private key: ${error.message}`); reject(error); return; } if (stderr && !stderr.includes('Generating EC parameters')) { console.error(`stderr: ${stderr}`); } // Generate public key from private key exec(`openssl ec -in ${PRIVATE_KEY_PATH} -pubout -out ${PUBLIC_KEY_PATH}`, (error, stdout, stderr) => { if (error) { console.error(`Error generating public key: ${error.message}`); reject(error); return; } if (stderr && !stderr.includes('read EC key')) { console.error(`stderr: ${stderr}`); } console.log('Successfully generated key pair'); resolve(); }); }); }); } /** * Authorize with Tesla API and get access token */ async function getAccessToken() { try { console.log('Getting Tesla API access token...'); const clientId = process.env.TESLA_CLIENT_ID; const clientSecret = process.env.TESLA_CLIENT_SECRET; const refreshToken = process.env.TESLA_REFRESH_TOKEN; if (!clientId || !clientSecret || !refreshToken) { throw new Error('Missing required environment variables'); } // Create form data const params = new URLSearchParams(); params.append('grant_type', 'refresh_token'); params.append('client_id', clientId); params.append('client_secret', clientSecret); params.append('refresh_token', refreshToken); params.append('scope', 'openid offline_access vehicle_device_data vehicle_cmds vehicle_charging_cmds'); const response = await axios.post('https://auth.tesla.com/oauth2/v3/token', params, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }); console.log('Successfully obtained access token'); return response.data.access_token; } catch (error: any) { console.error('Error getting access token:', error.response?.data || error.message); throw new Error('Failed to get access token'); } } /** * Register the application with Tesla API */ async function registerApplication(domain: string) { try { console.log(`Registering application with domain: ${domain}...`); const accessToken = await getAccessToken(); const response = await axios.post(`${BASE_URL}/api/1/partner_accounts`, { domain }, { headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' } } ); console.log('Application registered successfully with Tesla API!'); console.log('Response:', response.data); return response.data; } catch (error: any) { console.error('Error registering application:', error.response?.data || error.message); // Special handling for the "already registered" case which is not really an error if (error.response?.data?.error?.message?.includes('already registered')) { console.log('Application is already registered (this is not an error)'); return { status: 'already_registered' }; } throw new Error('Failed to register application'); } } /** * Check if the public key is properly registered */ async function checkPublicKeyRegistration(domain: string) { try { console.log(`Checking public key registration for domain: ${domain}...`); const accessToken = await getAccessToken(); const response = await axios.get(`${BASE_URL}/api/1/partner_accounts/public_key?domain=${encodeURIComponent(domain)}`, { headers: { 'Authorization': `Bearer ${accessToken}` } }); console.log('Public key registration status:', response.data); return response.data; } catch (error: any) { console.error('Error checking public key registration:', error.response?.data || error.message); throw new Error('Failed to check public key registration'); } } // Set up routes app.get(PUBLIC_KEY_ENDPOINT, (req: Request, res: Response) => { console.log('Public key requested'); if (fs.existsSync(PUBLIC_KEY_PATH)) { res.setHeader('Content-Type', 'application/x-pem-file'); res.sendFile(PUBLIC_KEY_PATH); } else { res.status(404).send('Public key not found'); } }); app.get('/status', (req: Request, res: Response) => { res.json({ status: 'OK', keys: { privateKeyExists: fs.existsSync(PRIVATE_KEY_PATH), publicKeyExists: fs.existsSync(PUBLIC_KEY_PATH) }, env: { clientIdExists: !!process.env.TESLA_CLIENT_ID, clientSecretExists: !!process.env.TESLA_CLIENT_SECRET, refreshTokenExists: !!process.env.TESLA_REFRESH_TOKEN } }); }); // Define the register route handler separately to resolve TypeScript error const registerHandler = async (req: Request, res: Response) => { try { const ngrokUrl = process.env.NGROK_URL; if (!ngrokUrl) { return res.status(400).json({ error: 'NGROK_URL environment variable not set' }); } const domain = new URL(ngrokUrl).hostname; // First check the public key registration const checkResult = await checkPublicKeyRegistration(domain).catch(() => null); // If public key is already registered, no need to register again if (checkResult && checkResult.public_key) { return res.json({ status: 'already_registered', message: 'Application is already registered with Tesla API', details: checkResult }); } // Register the application const result = await registerApplication(domain); res.json({ status: 'success', message: 'Application registered successfully with Tesla API', details: result }); } catch (error: any) { res.status(500).json({ status: 'error', message: error.message }); } }; app.get('/register', registerHandler); // Start server and ngrok async function startServer() { await generateKeyPair(); // Start the Express server app.listen(PORT, () => { console.log(`Server running on http://localhost:${PORT}`); }); try { // Start ngrok and create a tunnel to the Express server const url = await ngrok.connect({ addr: PORT, region: 'us' // Change if needed }); console.log(`ngrok tunnel created: ${url}`); console.log(`Public key URL: ${url}${PUBLIC_KEY_ENDPOINT}`); // Store ngrok URL in environment for registration process process.env.NGROK_URL = url; console.log('\nRegister your application:'); console.log('---------------------------'); console.log('1. Go to the Tesla Developer Portal: https://developer.tesla.com'); console.log('2. Update your application settings:'); console.log(` - Set Allowed Origin to: ${url}`); console.log('3. Once updated, open this URL in your browser to register:'); console.log(` ${url}/register`); } catch (error) { console.error('Error starting ngrok:', error); } } // Start the server startServer();