@coretext-ai/qa-google-contacts-a58500d5-8331-4ce9-a140-d204a9fae815
Version:
MCP server with google-contacts integration
185 lines • 7.18 kB
JavaScript
import { OAuth2Client } from 'google-auth-library';
import { TokenManager } from './token-manager.js';
import { CredentialLoader } from './credential-loader.js';
import open from 'open';
import http from 'http';
import url from 'url';
export class GoogleOAuthClient {
constructor() {
this.scopes = [
"https://www.googleapis.com/auth/contacts",
"https://www.googleapis.com/auth/contacts.other.readonly",
"https://www.googleapis.com/auth/contacts.readonly",
"https://www.googleapis.com/auth/directory.readonly",
"https://www.googleapis.com/auth/user.addresses.read",
"https://www.googleapis.com/auth/user.emails.read",
"https://www.googleapis.com/auth/user.phonenumbers.read",
"https://www.googleapis.com/auth/userinfo.profile"
];
this.tokenManager = new TokenManager();
this.credentialLoader = new CredentialLoader();
}
/**
* Initialize OAuth client
*/
async initialize() {
const credentialPath = process.env.GOOGLE_OAUTH_CREDENTIALS;
if (!credentialPath) {
throw new Error('GOOGLE_OAUTH_CREDENTIALS environment variable is required');
}
const credentials = await this.credentialLoader.loadCredentials(credentialPath);
this.oauth2Client = new OAuth2Client(credentials.client_id, credentials.client_secret, 'http://localhost:3000/oauth/callback');
console.error('[GOOGLE_OAUTH] OAuth client initialized');
}
/**
* Ensure we have a valid access token
*/
async getValidAccessToken() {
const tokens = await this.tokenManager.getTokens();
if (!tokens) {
console.error('[GOOGLE_OAUTH] No OAuth tokens found. Starting authorization flow...');
await this.authorize();
return this.getValidAccessToken();
}
if (this.isTokenExpired(tokens)) {
console.error('[GOOGLE_OAUTH] Token expired, refreshing...');
if (!tokens.refresh_token) {
throw new Error('No refresh token available for token refresh');
}
const refreshedTokens = await this.refreshTokens(tokens.refresh_token);
if (!refreshedTokens.access_token) {
throw new Error('Failed to refresh access token');
}
return refreshedTokens.access_token;
}
if (!tokens.access_token) {
throw new Error('No access token available');
}
return tokens.access_token;
}
/**
* Start OAuth authorization flow
*/
async authorize() {
const authUrl = this.oauth2Client.generateAuthUrl({
access_type: 'offline',
scope: this.scopes,
prompt: 'consent'
});
console.error('[GOOGLE_OAUTH] Starting OAuth authorization flow...');
console.error('[GOOGLE_OAUTH] Opening browser for authorization...');
console.error('[GOOGLE_OAUTH] If browser doesn\'t open, visit this URL:');
console.error(`[GOOGLE_OAUTH] ${authUrl}`);
// Start callback server and open browser in parallel
const [authCode] = await Promise.all([
this.startCallbackServer(),
open(authUrl)
]);
// Exchange the authorization code for tokens
await this.exchangeCodeForTokens(authCode);
}
/**
* Exchange authorization code for tokens
*/
async exchangeCodeForTokens(authCode) {
const { tokens } = await this.oauth2Client.getToken(authCode);
await this.tokenManager.storeTokens(tokens);
console.error('[GOOGLE_OAUTH] Authorization successful! Tokens stored.');
}
/**
* Refresh expired access token
*/
async refreshTokens(refreshToken) {
this.oauth2Client.setCredentials({ refresh_token: refreshToken });
const { credentials } = await this.oauth2Client.refreshAccessToken();
await this.tokenManager.storeTokens(credentials);
return credentials;
}
/**
* Check if token is expired (with 5-minute buffer)
*/
isTokenExpired(tokens) {
const buffer = 5 * 60 * 1000; // 5 minutes
const expiryTime = tokens.expiry_date || 0;
return Date.now() + buffer >= expiryTime;
}
/**
* Start a local HTTP server to handle OAuth callback
*/
async startCallbackServer() {
return new Promise((resolve, reject) => {
const server = http.createServer((req, res) => {
const queryObject = url.parse(req.url, true).query;
if (queryObject.code) {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(`
<html>
<body>
<h1>Authorization successful!</h1>
<p>You can close this window and return to your application.</p>
</body>
</html>
`);
server.close();
resolve(queryObject.code);
}
else if (queryObject.error) {
res.writeHead(400, { 'Content-Type': 'text/html' });
res.end(`
<html>
<body>
<h1>Authorization failed</h1>
<p>Error: ${queryObject.error}</p>
</body>
</html>
`);
server.close();
reject(new Error(`OAuth authorization failed: ${queryObject.error}`));
}
else {
res.writeHead(400, { 'Content-Type': 'text/plain' });
res.end('Invalid callback request');
}
});
// Listen on port 3000 (matching the redirect URI)
server.listen(3000, () => {
console.error('[GOOGLE_OAUTH] Callback server listening on http://localhost:3000');
});
// Timeout after 5 minutes
setTimeout(() => {
server.close();
reject(new Error('OAuth authorization timeout'));
}, 5 * 60 * 1000);
});
}
/**
* Revoke tokens and clear stored credentials
*/
async revokeTokens() {
const tokens = await this.tokenManager.getTokens();
if (tokens?.access_token) {
try {
await this.oauth2Client.revokeToken(tokens.access_token);
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.warn(`Failed to revoke token: ${errorMessage}`);
}
}
await this.tokenManager.deleteTokens();
console.log('OAuth tokens revoked and deleted');
}
/**
* Check if user has valid authentication
*/
async isAuthenticated() {
try {
const tokens = await this.tokenManager.getTokens();
return !!tokens?.access_token && !this.isTokenExpired(tokens);
}
catch (error) {
return false;
}
}
}
//# sourceMappingURL=google-oauth-client.js.map