Skip to main content
Glama
server.ts8.9 kB
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js"; import { OAuth2Client } from "google-auth-library"; // Import authentication components import { initializeOAuth2Client } from './auth/client.js'; import { AuthServer } from './auth/server.js'; import { TokenManager } from './auth/tokenManager.js'; // Import tool registry import { ToolRegistry } from './tools/registry.js'; // Import account management handler import { ManageAccountsHandler, ServerContext } from './handlers/core/ManageAccountsHandler.js'; import { z } from 'zod'; // Import transport handlers import { StdioTransportHandler } from './transports/stdio.js'; import { HttpTransportHandler, HttpTransportConfig } from './transports/http.js'; // Import config import { ServerConfig } from './config/TransportConfig.js'; export class GoogleCalendarMcpServer { private server: McpServer; private oauth2Client!: OAuth2Client; private tokenManager!: TokenManager; private authServer!: AuthServer; private config: ServerConfig; private accounts!: Map<string, OAuth2Client>; constructor(config: ServerConfig) { this.config = config; this.server = new McpServer({ name: "google-calendar", version: "1.3.0" }); } async initialize(): Promise<void> { // 1. Initialize Authentication (but don't block on it) this.oauth2Client = await initializeOAuth2Client(); this.tokenManager = new TokenManager(this.oauth2Client); this.authServer = new AuthServer(this.oauth2Client); // 2. Load all authenticated accounts this.accounts = await this.tokenManager.loadAllAccounts(); // 3. Handle startup authentication based on transport type await this.handleStartupAuthentication(); // 4. Set up Modern Tool Definitions this.registerTools(); // 5. Set up Graceful Shutdown this.setupGracefulShutdown(); } private async handleStartupAuthentication(): Promise<void> { // Skip authentication in test environment if (process.env.NODE_ENV === 'test') { return; } this.accounts = await this.tokenManager.loadAllAccounts(); if (this.accounts.size > 0) { const accountList = Array.from(this.accounts.keys()).join(', '); process.stderr.write(`Valid tokens found for account(s): ${accountList}\n`); return; } const accountMode = this.tokenManager.getAccountMode(); if (this.config.transport.type === 'stdio') { // For stdio mode, check for existing tokens const hasValidTokens = await this.tokenManager.validateTokens(accountMode); if (!hasValidTokens) { // No existing tokens - server will start but calendar tools won't work // User can authenticate via the 'add-account' tool process.stderr.write(`⚠️ No authenticated accounts found.\n`); process.stderr.write(`Use the 'add-account' tool to authenticate a Google account, or run:\n`); process.stderr.write(` npx @cocal/google-calendar-mcp auth\n\n`); // Don't exit - allow server to start so add-account tool is available } else { process.stderr.write(`Valid ${accountMode} user tokens found.\n`); this.accounts = await this.tokenManager.loadAllAccounts(); } } else { // For HTTP mode, check for tokens but don't block startup const hasValidTokens = await this.tokenManager.validateTokens(accountMode); if (!hasValidTokens) { process.stderr.write(`⚠️ No valid ${accountMode} user authentication tokens found.\n`); process.stderr.write('Visit the server URL in your browser to authenticate, or run "npm run auth" separately.\n'); } else { process.stderr.write(`Valid ${accountMode} user tokens found.\n`); this.accounts = await this.tokenManager.loadAllAccounts(); } } } private registerTools(): void { ToolRegistry.registerAll(this.server, this.executeWithHandler.bind(this)); // Register account management tools separately (they need special context) this.registerAccountManagementTools(); } /** * Register the manage-accounts tool that needs access to server internals. * This tool is special because it: * - Doesn't require existing authentication (for 'add' action) * - Needs access to authServer, tokenManager, etc. */ private registerAccountManagementTools(): void { const serverContext: ServerContext = { oauth2Client: this.oauth2Client, tokenManager: this.tokenManager, authServer: this.authServer, accounts: this.accounts, reloadAccounts: async () => { this.accounts = await this.tokenManager.loadAllAccounts(); return this.accounts; } }; const manageAccountsHandler = new ManageAccountsHandler(); this.server.tool( 'manage-accounts', "Manage Google account authentication. Actions: 'list' (show accounts), 'add' (authenticate new account), 'remove' (remove account).", { action: z.enum(['list', 'add', 'remove']) .describe("Action to perform: 'list' shows all accounts, 'add' authenticates a new account, 'remove' removes an account"), account_id: z.string() .regex(/^[a-z0-9_-]{1,64}$/, "Account nickname must be 1-64 characters: lowercase letters, numbers, dashes, underscores only") .optional() .describe("Account nickname (e.g., 'work', 'personal') - a friendly name to identify this Google account. Required for 'add' and 'remove'. Optional for 'list' (shows all if omitted)") }, async (args) => { return manageAccountsHandler.runTool(args, serverContext); } ); } private async ensureAuthenticated(): Promise<void> { const availableAccounts = await this.tokenManager.loadAllAccounts(); if (availableAccounts.size > 0) { this.accounts = availableAccounts; return; } // Check if we already have valid tokens if (await this.tokenManager.validateTokens()) { const refreshedAccounts = await this.tokenManager.loadAllAccounts(); if (refreshedAccounts.size > 0) { this.accounts = refreshedAccounts; return; } } // For stdio mode, authentication should have been handled at startup if (this.config.transport.type === 'stdio') { throw new McpError( ErrorCode.InvalidRequest, "Authentication tokens are no longer valid. Please restart the server to re-authenticate." ); } // For HTTP mode, try to start auth server if not already running try { const authSuccess = await this.authServer.start(false); // openBrowser = false for HTTP mode if (!authSuccess) { throw new McpError( ErrorCode.InvalidRequest, "Authentication required. Please run 'npm run auth' to authenticate, or visit the auth URL shown in the logs for HTTP mode." ); } } catch (error) { if (error instanceof McpError) { throw error; } if (error instanceof Error) { throw new McpError(ErrorCode.InvalidRequest, error.message); } throw new McpError(ErrorCode.InvalidRequest, "Authentication required. Please run 'npm run auth' to authenticate."); } } private async executeWithHandler(handler: any, args: any): Promise<{ content: Array<{ type: "text"; text: string }> }> { await this.ensureAuthenticated(); const result = await handler.runTool(args, this.accounts); return result; } async start(): Promise<void> { switch (this.config.transport.type) { case 'stdio': const stdioHandler = new StdioTransportHandler(this.server); await stdioHandler.connect(); break; case 'http': const httpConfig: HttpTransportConfig = { port: this.config.transport.port, host: this.config.transport.host }; const httpHandler = new HttpTransportHandler( this.server, httpConfig, this.tokenManager ); await httpHandler.connect(); break; default: throw new Error(`Unsupported transport type: ${this.config.transport.type}`); } } private setupGracefulShutdown(): void { const cleanup = async () => { try { if (this.authServer) { await this.authServer.stop(); } // McpServer handles transport cleanup automatically this.server.close(); process.exit(0); } catch (error: unknown) { process.stderr.write(`Error during cleanup: ${error instanceof Error ? error.message : error}\n`); process.exit(1); } }; process.on("SIGINT", cleanup); process.on("SIGTERM", cleanup); } // Expose server for testing getServer(): McpServer { return this.server; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/nspady/google-calendar-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server