=== NEW SERVERS ADDED (7) === - servers/closebot — 119 tools, 14 modules, 4,656 lines TS (Stage 7) - servers/google-console — Google Search Console MCP (Stage 7) - servers/meta-ads — Meta/Facebook Ads MCP (Stage 8) - servers/twilio — Twilio communications MCP (Stage 8) - servers/competitor-research — Competitive intel MCP (Stage 6) - servers/n8n-apps — n8n workflow MCP apps (Stage 6) - servers/reonomy — Commercial real estate MCP (Stage 1) === FACTORY INFRASTRUCTURE ADDED === - infra/factory-tools — mcp-jest, mcp-validator, mcp-add, MCP Inspector - 60 test configs, 702 auto-generated test cases - All 30 servers score 100/100 protocol compliance - infra/command-center — Pipeline state, operator playbook, dashboard config - infra/factory-reviews — Automated eval reports === DOCS ADDED === - docs/MCP-FACTORY.md — Factory overview - docs/reports/ — 5 pipeline evaluation reports - docs/research/ — Browser MCP research === RULES ESTABLISHED === - CONTRIBUTING.md — All MCP work MUST go in this repo - README.md — Full inventory of 37 servers + infra docs - .gitignore — Updated for Python venvs TOTAL: 37 MCP servers + full factory pipeline in one repo. This is now the single source of truth for all MCP work.
217 lines
5.6 KiB
TypeScript
217 lines
5.6 KiB
TypeScript
/**
|
|
* OAuth 2.0 authentication for Google Search Console
|
|
* Supports interactive browser flow with token persistence
|
|
*/
|
|
|
|
import { OAuth2Client } from 'google-auth-library';
|
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
import { homedir } from 'os';
|
|
import { join } from 'path';
|
|
import open from 'open';
|
|
import { createServer } from 'http';
|
|
|
|
const SCOPES = [
|
|
'https://www.googleapis.com/auth/webmasters',
|
|
'https://www.googleapis.com/auth/webmasters.readonly',
|
|
'https://www.googleapis.com/auth/indexing'
|
|
];
|
|
|
|
const TOKEN_DIR = join(homedir(), '.gsc-mcp');
|
|
const TOKEN_PATH = join(TOKEN_DIR, 'oauth-token.json');
|
|
const REDIRECT_URI = 'http://localhost:3000/oauth2callback';
|
|
|
|
export interface OAuthConfig {
|
|
clientId: string;
|
|
clientSecret: string;
|
|
redirectUri?: string;
|
|
}
|
|
|
|
/**
|
|
* Load OAuth credentials from environment or file
|
|
*/
|
|
export function loadOAuthConfig(): OAuthConfig | null {
|
|
// Try environment variables first
|
|
if (process.env.GSC_OAUTH_CLIENT_ID && process.env.GSC_OAUTH_CLIENT_SECRET) {
|
|
return {
|
|
clientId: process.env.GSC_OAUTH_CLIENT_ID,
|
|
clientSecret: process.env.GSC_OAUTH_CLIENT_SECRET,
|
|
redirectUri: process.env.GSC_OAUTH_REDIRECT_URI || REDIRECT_URI
|
|
};
|
|
}
|
|
|
|
// Try loading from file
|
|
if (process.env.GSC_OAUTH_CLIENT_FILE) {
|
|
try {
|
|
const content = readFileSync(process.env.GSC_OAUTH_CLIENT_FILE, 'utf-8');
|
|
const data = JSON.parse(content);
|
|
|
|
// Support both installed app and web app formats
|
|
const credentials = data.installed || data.web;
|
|
if (!credentials) {
|
|
throw new Error('Invalid OAuth client file format');
|
|
}
|
|
|
|
return {
|
|
clientId: credentials.client_id,
|
|
clientSecret: credentials.client_secret,
|
|
redirectUri: credentials.redirect_uris?.[0] || REDIRECT_URI
|
|
};
|
|
} catch (error: any) {
|
|
throw new Error(`Failed to load OAuth config from file: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Load stored OAuth tokens from disk
|
|
*/
|
|
function loadStoredTokens(): any | null {
|
|
if (!existsSync(TOKEN_PATH)) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
const content = readFileSync(TOKEN_PATH, 'utf-8');
|
|
return JSON.parse(content);
|
|
} catch (error) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save OAuth tokens to disk
|
|
*/
|
|
function saveTokens(tokens: any): void {
|
|
if (!existsSync(TOKEN_DIR)) {
|
|
mkdirSync(TOKEN_DIR, { recursive: true });
|
|
}
|
|
|
|
writeFileSync(TOKEN_PATH, JSON.stringify(tokens, null, 2));
|
|
}
|
|
|
|
/**
|
|
* Perform OAuth flow with browser-based consent
|
|
*/
|
|
async function performOAuthFlow(oauth2Client: OAuth2Client): Promise<void> {
|
|
const authUrl = oauth2Client.generateAuthUrl({
|
|
access_type: 'offline',
|
|
scope: SCOPES,
|
|
prompt: 'consent' // Force consent to get refresh token
|
|
});
|
|
|
|
console.error('Opening browser for Google authentication...');
|
|
console.error('If browser does not open, visit this URL:');
|
|
console.error(authUrl);
|
|
|
|
// Create temporary HTTP server to capture callback
|
|
const server = createServer();
|
|
const port = 3000;
|
|
|
|
const tokenPromise = new Promise<any>((resolve, reject) => {
|
|
server.on('request', async (req, res) => {
|
|
try {
|
|
const url = new URL(req.url || '', `http://localhost:${port}`);
|
|
|
|
if (url.pathname === '/oauth2callback') {
|
|
const code = url.searchParams.get('code');
|
|
|
|
if (!code) {
|
|
res.writeHead(400);
|
|
res.end('Missing authorization code');
|
|
reject(new Error('Missing authorization code'));
|
|
return;
|
|
}
|
|
|
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
res.end(`
|
|
<html>
|
|
<body>
|
|
<h1>Authentication successful!</h1>
|
|
<p>You can close this window and return to the terminal.</p>
|
|
</body>
|
|
</html>
|
|
`);
|
|
|
|
// Exchange code for tokens
|
|
const { tokens } = await oauth2Client.getToken(code);
|
|
resolve(tokens);
|
|
server.close();
|
|
}
|
|
} catch (error) {
|
|
reject(error);
|
|
server.close();
|
|
}
|
|
});
|
|
|
|
server.on('error', (error) => {
|
|
reject(error);
|
|
});
|
|
});
|
|
|
|
server.listen(port);
|
|
|
|
// Open browser
|
|
await open(authUrl);
|
|
|
|
// Wait for tokens
|
|
const tokens = await tokenPromise;
|
|
oauth2Client.setCredentials(tokens);
|
|
saveTokens(tokens);
|
|
}
|
|
|
|
/**
|
|
* Create authenticated OAuth2 client
|
|
*/
|
|
export async function getOAuthClient(): Promise<OAuth2Client> {
|
|
const config = loadOAuthConfig();
|
|
|
|
if (!config) {
|
|
throw new Error(
|
|
'OAuth credentials not found. Set GSC_OAUTH_CLIENT_ID/SECRET or GSC_OAUTH_CLIENT_FILE'
|
|
);
|
|
}
|
|
|
|
const oauth2Client = new OAuth2Client(
|
|
config.clientId,
|
|
config.clientSecret,
|
|
config.redirectUri
|
|
);
|
|
|
|
// Try to load stored tokens
|
|
const storedTokens = loadStoredTokens();
|
|
|
|
if (storedTokens) {
|
|
oauth2Client.setCredentials(storedTokens);
|
|
|
|
// Set up automatic token refresh
|
|
oauth2Client.on('tokens', (tokens) => {
|
|
if (tokens.refresh_token) {
|
|
saveTokens({ ...storedTokens, ...tokens });
|
|
} else {
|
|
saveTokens({ ...storedTokens, access_token: tokens.access_token });
|
|
}
|
|
});
|
|
|
|
// Verify tokens are still valid
|
|
try {
|
|
await oauth2Client.getAccessToken();
|
|
return oauth2Client;
|
|
} catch (error) {
|
|
console.error('Stored tokens invalid, re-authenticating...');
|
|
}
|
|
}
|
|
|
|
// Perform interactive OAuth flow
|
|
await performOAuthFlow(oauth2Client);
|
|
|
|
// Set up automatic token refresh
|
|
oauth2Client.on('tokens', (tokens) => {
|
|
const current = loadStoredTokens() || {};
|
|
saveTokens({ ...current, ...tokens });
|
|
});
|
|
|
|
return oauth2Client;
|
|
}
|