MCP Server Implementation
Complete implementation of the authentication automation MCP server
Complete MCP Auth Server
Build the complete MCP server that integrates all authentication concepts. This production-ready implementation handles multiple authentication patterns, session persistence, and Claude Code integration.
Project Structure
The auth automation MCP server is organized into modular components for maintainability. Each file handles a specific responsibility: server orchestration, browser automation, state persistence, and authentication strategies.
auth-automation-mcp/
├── server.js # MCP server entry point
├── playwright-auth.js # Playwright automation logic
├── state-manager.js # Session persistence & encryption
├── auth-strategies/ # Pluggable auth pattern handlers
│ ├── form-based.js
│ ├── saml-sso.js
│ └── oauth.js
├── config.example.json # Configuration template
├── package.json
├── .gitignore
└── examples/
├── university-portal.js
├── publisher-sso.js
└── library-proxy.jsPackage Configuration
Define dependencies and scripts for the MCP server:
{
"name": "auth-automation-mcp",
"version": "1.0.0",
"description": "MCP server for ethical authentication automation",
"main": "server.js",
"type": "module",
"scripts": {
"start": "node server.js",
"dev": "node --watch server.js"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^0.5.0",
"playwright": "^1.40.0",
"speakeasy": "^2.0.0",
"dotenv": "^16.3.1"
},
"engines": {
"node": ">=18.0.0"
}
}Prevent sensitive state files and credentials from being committed:
node_modules/
state/
config.json
.env
*.log
playwright-report/
test-results/Never commit the state directory or .env file. Session cookies and credentials must remain local to prevent security breaches. Use .env.example as a template for configuration.
Core MCP Server Implementation
The main server file orchestrates all MCP tools, handles authentication workflows, and manages error handling.
server.js implements the MCP protocol using stdio transport. It exposes five tools: authenticate (initial login), restore_session (reuse saved sessions), check_session (validate sessions), clear_session (delete sessions), and list_sessions (view all saved sessions).
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { PlaywrightAuthManager } from './playwright-auth.js';
import { StateManager } from './state-manager.js';
import dotenv from 'dotenv';
dotenv.config();
class AuthAutomationServer {
constructor() {
this.server = new Server(
{
name: 'auth-automation-mcp',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
this.stateManager = new StateManager({
stateDir: './state',
encryptionKey: process.env.STATE_ENCRYPTION_KEY || 'default-key-change-me'
});
this.authManager = new PlaywrightAuthManager(this.stateManager);
this.setupToolHandlers();
// Error handling
this.server.onerror = (error) => console.error('[MCP Error]', error);
process.on('SIGINT', async () => {
await this.authManager.cleanup();
process.exit(0);
});
}
setupToolHandlers() {
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'authenticate',
description: 'Authenticate to a website and save session state. Supports form-based, SSO/SAML, and OAuth flows.',
inputSchema: {
type: 'object',
properties: {
site: {
type: 'string',
description: 'Site identifier (e.g., "university-library", "sciencedirect")',
},
url: {
type: 'string',
description: 'Login page URL',
},
strategy: {
type: 'string',
enum: ['form', 'saml', 'oauth'],
description: 'Authentication strategy to use',
},
credentials: {
type: 'object',
properties: {
username: { type: 'string' },
password: { type: 'string' },
totpSecret: { type: 'string' },
},
description: 'Authentication credentials (prefer environment variables)',
},
interactive: {
type: 'boolean',
description: 'Launch browser in interactive mode for MFA/CAPTCHA',
default: false,
},
},
required: ['site', 'url', 'strategy'],
},
},
{
name: 'restore_session',
description: 'Restore previously authenticated session and navigate to URL',
inputSchema: {
type: 'object',
properties: {
site: {
type: 'string',
description: 'Site identifier from previous authentication',
},
url: {
type: 'string',
description: 'URL to navigate to after restoring session',
},
},
required: ['site', 'url'],
},
},
{
name: 'check_session',
description: 'Check if saved session is still valid',
inputSchema: {
type: 'object',
properties: {
site: {
type: 'string',
description: 'Site identifier to check',
},
},
required: ['site'],
},
},
{
name: 'clear_session',
description: 'Clear saved session state for a site',
inputSchema: {
type: 'object',
properties: {
site: {
type: 'string',
description: 'Site identifier to clear',
},
},
required: ['site'],
},
},
{
name: 'list_sessions',
description: 'List all saved authentication sessions',
inputSchema: {
type: 'object',
properties: {},
},
},
],
}));
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'authenticate':
return await this.handleAuthenticate(args);
case 'restore_session':
return await this.handleRestoreSession(args);
case 'check_session':
return await this.handleCheckSession(args);
case 'clear_session':
return await this.handleClearSession(args);
case 'list_sessions':
return await this.handleListSessions();
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error: ${error.message}`,
},
],
isError: true,
};
}
});
}
async handleAuthenticate(args) {
const { site, url, strategy, credentials, interactive = false } = args;
// Use environment variables if credentials not provided
const finalCredentials = {
username: credentials?.username || process.env[`${site.toUpperCase()}_USERNAME`],
password: credentials?.password || process.env[`${site.toUpperCase()}_PASSWORD`],
totpSecret: credentials?.totpSecret || process.env[`${site.toUpperCase()}_TOTP_SECRET`],
};
if (!finalCredentials.username || !finalCredentials.password) {
throw new Error(`Missing credentials for ${site}. Set ${site.toUpperCase()}_USERNAME and ${site.toUpperCase()}_PASSWORD environment variables.`);
}
const result = await this.authManager.authenticate({
site,
url,
strategy,
credentials: finalCredentials,
interactive,
});
if (result.success) {
return {
content: [
{
type: 'text',
text: `Successfully authenticated to ${site}. Session saved and ready for reuse.`,
},
],
};
} else {
throw new Error(result.error || 'Authentication failed');
}
}
async handleRestoreSession(args) {
const { site, url } = args;
const result = await this.authManager.restoreSession(site, url);
if (result.success) {
return {
content: [
{
type: 'text',
text: `Session restored for ${site}. Navigated to ${url}. Status: ${result.authenticated ? 'Authenticated' : 'Session may be expired'}`,
},
],
};
} else {
throw new Error(result.error || 'Failed to restore session');
}
}
async handleCheckSession(args) {
const { site } = args;
const valid = await this.stateManager.hasValidSession(site);
return {
content: [
{
type: 'text',
text: `Session for ${site}: ${valid ? 'Valid' : 'Invalid or expired'}`,
},
],
};
}
async handleClearSession(args) {
const { site } = args;
await this.stateManager.clearSession(site);
return {
content: [
{
type: 'text',
text: `Cleared session for ${site}`,
},
],
};
}
async handleListSessions() {
const sessions = await this.stateManager.listSessions();
const sessionInfo = sessions.map(s =>
`- ${s.site}: Last authenticated ${new Date(s.timestamp).toLocaleString()}, ` +
`${s.cookieCount} cookies, ${s.valid ? 'Valid' : 'Expired'}`
).join('\n');
return {
content: [
{
type: 'text',
text: `Saved sessions:\n${sessionInfo || '(none)'}`,
},
],
};
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('Auth Automation MCP server running on stdio');
}
}
const server = new AuthAutomationServer();
server.run().catch(console.error);Playwright Authentication Manager
The authentication manager handles browser automation, credential filling, MFA detection, and session verification across different authentication strategies.
import { chromium } from 'playwright';
import speakeasy from 'speakeasy';
import readline from 'readline';
export class PlaywrightAuthManager {
constructor(stateManager) {
this.stateManager = stateManager;
this.browser = null;
this.context = null;
}
async initialize(interactive = false) {
if (this.browser) return;
this.browser = await chromium.launch({
headless: !interactive,
args: ['--disable-blink-features=AutomationControlled'],
});
}
async cleanup() {
if (this.context) {
await this.context.close();
this.context = null;
}
if (this.browser) {
await this.browser.close();
this.browser = null;
}
}
async authenticate({ site, url, strategy, credentials, interactive }) {
await this.initialize(interactive);
this.context = await this.browser.newContext({
viewport: { width: 1280, height: 720 },
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
});
const page = await this.context.newPage();
try {
console.error(`[${site}] Starting ${strategy} authentication...`);
let success = false;
switch (strategy) {
case 'form':
success = await this.authenticateFormBased(page, url, credentials);
break;
case 'saml':
success = await this.authenticateSAML(page, url, credentials);
break;
case 'oauth':
success = await this.authenticateOAuth(page, url, credentials);
break;
default:
throw new Error(`Unknown strategy: ${strategy}`);
}
if (success) {
// Save state
const cookies = await this.context.cookies();
const localStorage = await page.evaluate(() => {
return JSON.stringify(window.localStorage);
});
await this.stateManager.saveSession(site, {
cookies,
localStorage,
url,
timestamp: new Date().toISOString(),
});
console.error(`[${site}] Authentication successful, state saved`);
return { success: true };
} else {
return { success: false, error: 'Authentication verification failed' };
}
} catch (error) {
console.error(`[${site}] Authentication error:`, error.message);
return { success: false, error: error.message };
} finally {
await page.close();
await this.cleanup();
}
}
async authenticateFormBased(page, url, credentials) {
await page.goto(url, { waitUntil: 'networkidle' });
// Wait for login form
await page.waitForSelector('input[type="password"]', { timeout: 10000 });
// Find and fill username field (common selectors)
const usernameSelectors = [
'input[name="username"]',
'input[name="email"]',
'input[name="user"]',
'input[type="email"]',
'input[id="username"]',
'input[id="email"]',
];
for (const selector of usernameSelectors) {
if (await page.locator(selector).count() > 0) {
await page.fill(selector, credentials.username);
break;
}
}
// Fill password field
await page.fill('input[type="password"]', credentials.password);
// Click submit button
const submitSelectors = [
'button[type="submit"]',
'input[type="submit"]',
'button:has-text("Sign In")',
'button:has-text("Log In")',
'button:has-text("Login")',
];
for (const selector of submitSelectors) {
if (await page.locator(selector).count() > 0) {
await page.click(selector);
break;
}
}
// Wait for navigation or MFA prompt
await page.waitForLoadState('networkidle');
// Check for MFA/TOTP prompt
if (await page.locator('input[name*="code"], input[name*="token"]').count() > 0) {
await this.handleMFA(page, credentials);
}
// Verify authentication succeeded
return await this.verifyAuthentication(page, url);
}
async authenticateSAML(page, url, credentials) {
await page.goto(url, { waitUntil: 'networkidle' });
// Click institutional login link (common patterns)
const institutionalLoginSelectors = [
'a:has-text("Institutional Login")',
'a:has-text("Sign in through your institution")',
'button:has-text("Institution")',
'a[href*="sso"]',
'a[href*="saml"]',
];
for (const selector of institutionalLoginSelectors) {
if (await page.locator(selector).count() > 0) {
await page.click(selector);
break;
}
}
// Wait for redirect to IdP
await page.waitForLoadState('networkidle');
// Now on IdP login page - authenticate
await page.waitForSelector('input[type="password"]');
await page.fill('input[name="username"], input[name="email"]', credentials.username);
await page.fill('input[type="password"]', credentials.password);
await page.click('button[type="submit"]');
// Wait for SAML redirect back to SP
await page.waitForLoadState('networkidle');
// Check for MFA
if (await page.locator('input[name*="code"]').count() > 0) {
await this.handleMFA(page, credentials);
await page.waitForLoadState('networkidle');
}
return await this.verifyAuthentication(page, url);
}
async authenticateOAuth(page, url, credentials) {
await page.goto(url, { waitUntil: 'networkidle' });
// Click OAuth login button
const oauthSelectors = [
'button:has-text("Sign in with")',
'a:has-text("Continue with")',
'button[class*="oauth"]',
];
for (const selector of oauthSelectors) {
if (await page.locator(selector).count() > 0) {
await page.click(selector);
break;
}
}
await page.waitForLoadState('networkidle');
// Authenticate on OAuth provider page
await page.fill('input[name="username"], input[name="email"]', credentials.username);
await page.fill('input[type="password"]', credentials.password);
await page.click('button[type="submit"]');
await page.waitForLoadState('networkidle');
// Handle consent screen if present
if (await page.locator('button:has-text("Allow"), button:has-text("Authorize")').count() > 0) {
await page.click('button:has-text("Allow"), button:has-text("Authorize")');
await page.waitForLoadState('networkidle');
}
return await this.verifyAuthentication(page, url);
}
async handleMFA(page, credentials) {
console.error('MFA required...');
if (credentials.totpSecret) {
// Generate TOTP code
const token = speakeasy.totp({
secret: credentials.totpSecret,
encoding: 'base32',
});
console.error(`Generated TOTP code: ${token}`);
await page.fill('input[name*="code"], input[name*="token"]', token);
await page.click('button[type="submit"]');
await page.waitForLoadState('networkidle');
} else {
// Interactive MFA - wait for user
console.error('Please complete MFA in the browser window...');
await page.waitForNavigation({ timeout: 120000 }).catch(() => {
console.error('MFA timeout - assuming user completed');
});
}
}
async verifyAuthentication(page, originalUrl) {
const currentUrl = page.url();
// Check if we're NOT on login page anymore
if (currentUrl.includes('/login') || currentUrl.includes('/signin')) {
// Still on login page - check for error messages
const errorText = await page.textContent('body');
if (errorText.includes('invalid') || errorText.includes('incorrect')) {
return false;
}
}
// Look for authenticated user indicators
const authIndicators = [
'.user-profile',
'.user-menu',
'a:has-text("Logout")',
'a:has-text("Sign Out")',
'[data-testid="user-menu"]',
];
for (const selector of authIndicators) {
if (await page.locator(selector).count() > 0) {
return true;
}
}
// Check cookies for session indicators
const cookies = await page.context().cookies();
const sessionCookies = cookies.filter(c =>
c.name.toLowerCase().includes('session') ||
c.name.toLowerCase().includes('auth') ||
c.name.toLowerCase().includes('token')
);
return sessionCookies.length > 0;
}
async restoreSession(site, url) {
await this.initialize(false);
const state = await this.stateManager.loadSession(site);
if (!state) {
return { success: false, error: 'No saved session found' };
}
this.context = await this.browser.newContext();
await this.context.addCookies(state.cookies);
const page = await this.context.newPage();
// Restore localStorage
await page.goto(url);
await page.evaluate((storageData) => {
const items = JSON.parse(storageData);
for (const [key, value] of Object.entries(items)) {
window.localStorage.setItem(key, value);
}
}, state.localStorage);
// Reload to apply storage
await page.reload({ waitUntil: 'networkidle' });
// Verify session is still valid
const authenticated = await this.verifyAuthentication(page, url);
await page.close();
await this.cleanup();
return { success: true, authenticated };
}
}State Manager with Encryption
The state manager handles secure storage of session data using AES-256-CBC encryption with restrictive file permissions.
Session state files contain authentication cookies and localStorage data. The StateManager encrypts all state files using AES-256-CBC before writing to disk. Set a strong STATE_ENCRYPTION_KEY in your .env file and never commit state files to version control.
import fs from 'fs/promises';
import path from 'path';
import crypto from 'crypto';
export class StateManager {
constructor({ stateDir, encryptionKey }) {
this.stateDir = stateDir;
this.encryptionKey = crypto.scryptSync(encryptionKey, 'auth-mcp-salt', 32);
this.initializeStateDir();
}
async initializeStateDir() {
try {
await fs.mkdir(this.stateDir, { recursive: true });
// Set restrictive permissions (Unix-like systems)
if (process.platform !== 'win32') {
await fs.chmod(this.stateDir, 0o700);
}
} catch (error) {
console.error('Failed to initialize state directory:', error);
}
}
encrypt(data) {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv('aes-256-cbc', this.encryptionKey, iv);
let encrypted = cipher.update(JSON.stringify(data), 'utf8', 'hex');
encrypted += cipher.final('hex');
return {
iv: iv.toString('hex'),
data: encrypted,
};
}
decrypt(encrypted) {
const iv = Buffer.from(encrypted.iv, 'hex');
const decipher = crypto.createDecipheriv('aes-256-cbc', this.encryptionKey, iv);
let decrypted = decipher.update(encrypted.data, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return JSON.parse(decrypted);
}
getStatePath(site) {
return path.join(this.stateDir, `${site}.encrypted.json`);
}
async saveSession(site, state) {
const encrypted = this.encrypt(state);
const filepath = this.getStatePath(site);
await fs.writeFile(filepath, JSON.stringify(encrypted, null, 2));
// Set restrictive file permissions
if (process.platform !== 'win32') {
await fs.chmod(filepath, 0o600);
}
console.error(`[StateManager] Saved session for ${site}`);
}
async loadSession(site) {
const filepath = this.getStatePath(site);
try {
const encrypted = JSON.parse(await fs.readFile(filepath, 'utf-8'));
const state = this.decrypt(encrypted);
console.error(`[StateManager] Loaded session for ${site}`);
return state;
} catch (error) {
console.error(`[StateManager] Failed to load session for ${site}:`, error.message);
return null;
}
}
async hasValidSession(site) {
const state = await this.loadSession(site);
if (!state) return false;
// Check if cookies are expired
const validCookies = state.cookies.filter(cookie => {
if (!cookie.expires) return true; // Session cookie
return cookie.expires * 1000 > Date.now();
});
return validCookies.length > 0;
}
async clearSession(site) {
const filepath = this.getStatePath(site);
try {
await fs.unlink(filepath);
console.error(`[StateManager] Cleared session for ${site}`);
} catch (error) {
if (error.code !== 'ENOENT') {
console.error(`[StateManager] Failed to clear session for ${site}:`, error.message);
}
}
}
async listSessions() {
try {
const files = await fs.readdir(this.stateDir);
const sessions = [];
for (const file of files) {
if (!file.endsWith('.encrypted.json')) continue;
const site = file.replace('.encrypted.json', '');
const state = await this.loadSession(site);
if (state) {
const valid = state.cookies.some(c =>
!c.expires || c.expires * 1000 > Date.now()
);
sessions.push({
site,
timestamp: state.timestamp,
cookieCount: state.cookies.length,
valid,
});
}
}
return sessions;
} catch (error) {
console.error('[StateManager] Failed to list sessions:', error.message);
return [];
}
}
}Configuration Templates
Define site-specific authentication configurations:
{
"sites": {
"university-library": {
"url": "https://library.university.edu/login",
"strategy": "saml",
"description": "University library system with SSO"
},
"sciencedirect": {
"url": "https://www.sciencedirect.com/",
"strategy": "saml",
"description": "Elsevier ScienceDirect via institutional access"
},
"ieee-xplore": {
"url": "https://ieeexplore.ieee.org/",
"strategy": "form",
"description": "IEEE Xplore with personal account"
}
},
"defaults": {
"timeout": 30000,
"headless": true,
"maxRetries": 3
}
}Store credentials securely in environment variables:
# State encryption (generate secure random key)
STATE_ENCRYPTION_KEY=your-secure-encryption-key-here
# University credentials
UNIVERSITY_LIBRARY_USERNAME=your.email@university.edu
UNIVERSITY_LIBRARY_PASSWORD=your-password
UNIVERSITY_LIBRARY_TOTP_SECRET=your-totp-secret-if-using-2fa
# Publisher accounts
SCIENCEDIRECT_USERNAME=your.email@university.edu
SCIENCEDIRECT_PASSWORD=your-password
IEEE_XPLORE_USERNAME=your-ieee-account
IEEE_XPLORE_PASSWORD=your-password
IEEE_XPLORE_TOTP_SECRET=your-totp-secret
# Optional: Proxy settings if required
# HTTP_PROXY=http://proxy.university.edu:8080
# HTTPS_PROXY=http://proxy.university.edu:8080Generate a secure encryption key using: node -e "console.log(require('crypto').randomBytes(32).toString('base64'))". Copy the .env.example file to .env and replace placeholder values with your actual credentials. Never commit .env to version control.
Next Steps
With the complete MCP server implementation in place, the next chapter covers Claude Code integration and practical usage workflows for automating academic research authentication.