Tesla MCP Server
by scald
- src
/**
* Utility script to obtain a Tesla API refresh token
* Following the official Tesla Fleet API OAuth flow
*/
import axios from 'axios';
import dotenv from 'dotenv';
import * as http from 'http';
import { URL } from 'url';
import { exec } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
import * as crypto from 'crypto';
// Get current file's directory in ESM
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Load environment variables
dotenv.config();
// Get environment variables
const clientId = process.env.TESLA_CLIENT_ID;
const clientSecret = process.env.TESLA_CLIENT_SECRET;
if (!clientId || !clientSecret) {
console.error('Error: TESLA_CLIENT_ID and TESLA_CLIENT_SECRET must be set in .env file');
process.exit(1);
}
// Constants
const AUTH_URL = 'https://auth.tesla.com/oauth2/v3';
const PORT = 3000;
const REDIRECT_URI = `http://localhost:${PORT}/callback`;
const SCOPES = 'openid offline_access vehicle_device_data vehicle_cmds vehicle_charging_cmds';
// Generate PKCE code verifier and challenge
function generateCodeVerifier() {
return crypto.randomBytes(32).toString('base64url');
}
function generateCodeChallenge(verifier: string) {
return crypto.createHash('sha256').update(verifier).digest('base64url');
}
// Generate random state for security
const state = crypto.randomBytes(16).toString('base64url');
const codeVerifier = generateCodeVerifier();
const codeChallenge = generateCodeChallenge(codeVerifier);
// Authorization URL
const authUrl = `${AUTH_URL}/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(REDIRECT_URI)}&response_type=code&scope=${encodeURIComponent(SCOPES)}&state=${state}&code_challenge=${codeChallenge}&code_challenge_method=S256`;
// Debug output
console.log('\n==== DEBUG INFO ====');
console.log('Client ID:', clientId);
console.log('Redirect URI:', REDIRECT_URI);
console.log('Code Verifier:', codeVerifier);
console.log('Code Challenge:', codeChallenge);
console.log('Full Auth URL:', authUrl);
console.log('====================\n');
// Open the browser for the user to authenticate
console.log('Opening browser for Tesla authentication...');
console.log('Please log in with your Tesla account when the browser opens.');
console.log('\nIf the browser doesn\'t open automatically, paste this URL into your browser:');
console.log(authUrl);
// Open the URL in the default browser
try {
const command = process.platform === 'darwin' ? 'open' :
process.platform === 'win32' ? 'start' : 'xdg-open';
exec(`${command} "${authUrl}"`);
} catch (error: any) {
console.log('Failed to open browser automatically. Please open the URL manually.');
}
// Create a simple HTTP server to handle the callback
const server = http.createServer(async (req, res) => {
if (!req.url) {
return;
}
const parsedUrl = new URL(req.url, `http://${req.headers.host}`);
if (parsedUrl.pathname === '/callback') {
const code = parsedUrl.searchParams.get('code');
const error = parsedUrl.searchParams.get('error');
const returnedState = parsedUrl.searchParams.get('state');
// Close response with a success message
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(`
<html>
<body>
<h1>Tesla API Authentication</h1>
<p>You can close this window and return to your terminal.</p>
</body>
</html>
`);
// Handle errors
if (error) {
console.error(`Authentication error: ${error}`);
server.close();
process.exit(1);
}
// Verify state to prevent CSRF attacks
if (returnedState !== state) {
console.error('Error: State mismatch. Possible CSRF attack.');
server.close();
process.exit(1);
}
if (code) {
try {
console.log('\nExchanging authorization code for tokens...');
// Create form data for the request
const params = new URLSearchParams();
params.append('grant_type', 'authorization_code');
params.append('client_id', clientId);
params.append('client_secret', clientSecret);
params.append('code', code);
params.append('code_verifier', codeVerifier);
params.append('redirect_uri', REDIRECT_URI);
// Exchange the code for tokens using form URL encoding
const tokenResponse = await axios.post(`${AUTH_URL}/token`, params, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
});
const { access_token, refresh_token, expires_in } = tokenResponse.data;
console.log('\nAuthentication successful!\n');
console.log('Access Token:', access_token.substring(0, 20) + '...');
console.log('Refresh Token:', refresh_token);
console.log('Token expires in:', expires_in, 'seconds');
console.log('\nThis refresh token does not expire unless revoked.');
// Update the .env file with the refresh token
try {
const envPath = path.resolve(process.cwd(), '.env');
let envContent = fs.readFileSync(envPath, 'utf8');
// Replace or add the refresh token
if (envContent.includes('TESLA_REFRESH_TOKEN=')) {
envContent = envContent.replace(
/TESLA_REFRESH_TOKEN=.*/,
`TESLA_REFRESH_TOKEN=${refresh_token}`
);
} else {
envContent += `\nTESLA_REFRESH_TOKEN=${refresh_token}\n`;
}
fs.writeFileSync(envPath, envContent);
console.log('\nThe refresh token has been saved to your .env file.');
} catch (err) {
console.error('Failed to update .env file. Please update it manually with the refresh token above.');
}
} catch (error: any) {
console.error('\nError exchanging authorization code for tokens:');
if (error.response) {
console.error('Response status:', error.response.status);
console.error('Response data:', JSON.stringify(error.response.data, null, 2));
// Specific error handling for common issues
if (error.response.data && error.response.data.error === 'invalid_grant') {
console.error('\nThe authorization code is invalid or expired. Please try again.');
} else if (error.response.data && error.response.data.error === 'invalid_request') {
console.error('\nInvalid request. Check if the redirect_uri matches exactly what is configured in the Tesla Developer Console.');
}
} else if (error.request) {
console.error('No response received from server:', error.request);
} else {
console.error('Error message:', error.message);
}
}
// Close the server
server.close();
process.exit(0);
}
}
});
// Attempt to start the server with error handling for port conflicts
function startServer() {
server.on('error', (error: Error & { code?: string }) => {
if (error.code === 'EADDRINUSE') {
console.error(`\nError: Port ${PORT} is already in use.`);
console.error('The MCP server is likely running on this port.');
console.error('Please stop the MCP server before running this script, or update your Tesla Developer Portal settings to use a different port.');
process.exit(1);
} else {
console.error('Server error:', error);
process.exit(1);
}
});
// Start the server
server.listen(PORT, () => {
console.log(`\nListening for Tesla API callback on http://localhost:${PORT}`);
});
}
startServer();