youtube-analytics-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/README.md +237 -0
  2. package/build/auth/auth-manager.js +176 -0
  3. package/build/auth/tool-configs.js +57 -0
  4. package/build/auth/types.js +27 -0
  5. package/build/index.js +115 -0
  6. package/build/prompt-configs.js +145 -0
  7. package/build/server/info-configs.js +42 -0
  8. package/build/tool-configs.js +18 -0
  9. package/build/types.js +27 -0
  10. package/build/utils/formatters/audience.js +122 -0
  11. package/build/utils/formatters/channel.js +120 -0
  12. package/build/utils/formatters/discovery.js +165 -0
  13. package/build/utils/formatters/engagement.js +126 -0
  14. package/build/utils/formatters/health.js +155 -0
  15. package/build/utils/formatters/index.js +6 -0
  16. package/build/utils/formatters/performance.js +181 -0
  17. package/build/utils/index.js +3 -0
  18. package/build/utils/parsers/analytics.js +100 -0
  19. package/build/utils/parsers/index.js +1 -0
  20. package/build/utils/transformers/analytics.js +53 -0
  21. package/build/utils/transformers/index.js +3 -0
  22. package/build/utils/transformers/statistics.js +37 -0
  23. package/build/utils/transformers/thumbnails.js +26 -0
  24. package/build/youtube/tools/audience-configs.js +165 -0
  25. package/build/youtube/tools/channel-configs.js +113 -0
  26. package/build/youtube/tools/discovery-configs.js +109 -0
  27. package/build/youtube/tools/engagement-configs.js +94 -0
  28. package/build/youtube/tools/health-configs.js +314 -0
  29. package/build/youtube/tools/performance-configs.js +250 -0
  30. package/build/youtube/types.js +13 -0
  31. package/build/youtube/youtube-client.js +579 -0
  32. package/package.json +31 -0
package/README.md ADDED
@@ -0,0 +1,237 @@
1
+ # YouTube Analytics MCP Server
2
+
3
+ A Model Context Protocol (MCP) server for YouTube Analytics data access with demographics and discovery tools, built with a scalable config-driven architecture.
4
+
5
+ ## Core Features
6
+
7
+ - **Channel Analytics**: Get comprehensive channel overview, growth patterns, and vital signs
8
+ - **Video Performance**: Analyze individual video metrics, audience retention, and drop-off points
9
+ - **Viewer Demographics**: Access age/gender breakdowns and geographic distribution data
10
+ - **Discovery Insights**: Understand traffic sources and search terms driving views
11
+ - **Engagement Metrics**: Track likes, comments, shares, and viewer interaction patterns
12
+ - **Audience Retention**: Identify exact moments where viewers drop off for content optimization
13
+ - **Performance Comparison**: Compare metrics between different time periods
14
+ - **Public Channel Analysis**: Research competitor channels and trending content
15
+
16
+ ## Core Architecture Principles
17
+
18
+ This MCP server follows a **config-driven architecture** that provides:
19
+
20
+ - **Maintainability**: Clear separation between tool definitions and implementation
21
+ - **Scalability**: Easy to add new tools without modifying core server logic
22
+ - **Consistency**: Standardized error handling and response formatting
23
+ - **Readability**: Clean, declarative configuration that serves as documentation
24
+
25
+ ## Setup
26
+
27
+ ### 1. Google API Credentials
28
+
29
+ To use this YouTube Analytics MCP server, you need to set up Google API credentials:
30
+
31
+ 1. Go to the [Google Cloud Console](https://console.cloud.google.com/)
32
+ 2. Create a new project or select an existing one
33
+ 3. Enable the YouTube Analytics API and YouTube Data API v3
34
+ 4. Go to "Credentials" and create a new OAuth 2.0 Client ID
35
+ 5. Download the credentials as JSON
36
+ 6. Save the file as `credentials.json` in the `src/auth/` directory
37
+
38
+ **Privacy Note**: All data processing happens locally on your computer. Your credentials and analytics data never leave your machine - the server runs entirely locally and connects directly to Google's APIs from your system.
39
+
40
+ ### 2. Development
41
+
42
+ ```bash
43
+ # Install dependencies
44
+ npm install
45
+
46
+ # Build the project
47
+ npm run build
48
+
49
+ # Run in development mode
50
+ npm run dev
51
+
52
+ # Inspect with MCP Inspector
53
+ npm run inspect
54
+ ```
55
+
56
+ ## Architecture Overview
57
+
58
+ ## Project Structure
59
+
60
+ ```
61
+ src/
62
+ ├── index.ts # Main server entry point (config-driven)
63
+ ├── tool-configs.ts # Central tool configuration aggregator
64
+ ├── types.ts # TypeScript interfaces and types
65
+ ├── auth/
66
+ │ ├── tool-configs.ts # Authentication tool configurations
67
+ │ └── ...
68
+ ├── server/
69
+ │ ├── info-configs.ts # Server info tool configurations
70
+ │ └── ...
71
+ └── youtube/tools/
72
+ ├── channel-configs.ts # Channel analysis tool configurations
73
+ ├── health-configs.ts # Channel health tool configurations
74
+ ├── audience-configs.ts # Audience demographics tool configurations
75
+ ├── discovery-configs.ts # Traffic source tool configurations
76
+ ├── performance-configs.ts # Performance analysis tool configurations
77
+ └── engagement-configs.ts # Engagement metrics tool configurations
78
+ ```
79
+
80
+ ## Tool Configuration Structure
81
+
82
+ Each tool is defined by a configuration object:
83
+
84
+ ```typescript
85
+ interface ToolConfig<T = any> {
86
+ name: string; // Tool name
87
+ description: string; // Tool description
88
+ schema: any; // Zod schema for validation
89
+ handler: (params: T, context: ToolContext) => Promise<ToolResult>;
90
+ category?: string; // Optional grouping
91
+ }
92
+ ```
93
+
94
+ ## Available Tools
95
+
96
+ ### Authentication Tools
97
+ - `check_auth_status` - Check YouTube authentication status
98
+ - `revoke_auth` - Revoke authentication and clear tokens
99
+
100
+ ### Channel Tools
101
+ - `get_channel_info` - Get basic channel information
102
+ - `get_channel_videos` - Get list of channel videos with filters
103
+
104
+ ### Health Tools
105
+ - `get_channel_overview` - Get channel vital signs and growth patterns
106
+ - `get_comparison_metrics` - Compare metrics between time periods
107
+ - `get_average_view_percentage` - Get average view percentage
108
+
109
+ ### Audience Tools
110
+ - `get_video_demographics` - Get age/gender breakdown
111
+ - `get_geographic_distribution` - Get viewer geographic distribution
112
+ - `get_subscriber_analytics` - Get subscriber vs non-subscriber analytics
113
+
114
+ ### Discovery Tools
115
+ - `get_traffic_sources` - Get traffic source analysis
116
+ - `get_search_terms` - Get search terms for SEO insights
117
+
118
+ ### Performance Tools
119
+ - `get_audience_retention` - Track viewer retention patterns
120
+ - `get_retention_dropoff_points` - Find exact drop-off moments
121
+
122
+ ### Engagement Tools
123
+ - `get_engagement_metrics` - Analyze likes, comments, and shares
124
+
125
+ ## Adding New Tools
126
+
127
+ To add a new tool, simply create a configuration object and add it to the appropriate config file:
128
+
129
+ ```typescript
130
+ // In src/youtube/tools/new-category-configs.ts
131
+ export const newCategoryToolConfigs: ToolConfig[] = [
132
+ {
133
+ name: "new_tool_name",
134
+ description: "Description of what the tool does",
135
+ category: "new_category",
136
+ schema: z.object({
137
+ // Define your parameters here
138
+ param1: z.string().describe("Description of parameter 1"),
139
+ param2: z.number().optional().describe("Optional parameter 2"),
140
+ }),
141
+ handler: async ({ param1, param2 }, { getYouTubeClient }: ToolContext) => {
142
+ try {
143
+ const youtubeClient = await getYouTubeClient();
144
+ // Your tool implementation here
145
+
146
+ return {
147
+ content: [{
148
+ type: "text",
149
+ text: "Tool result here"
150
+ }]
151
+ };
152
+ } catch (error) {
153
+ return {
154
+ content: [{
155
+ type: "text",
156
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
157
+ }],
158
+ isError: true
159
+ };
160
+ }
161
+ },
162
+ },
163
+ ];
164
+
165
+ // Then add to src/tool-configs.ts
166
+ export const allToolConfigs = [
167
+ // ... existing configs
168
+ ...newCategoryToolConfigs,
169
+ ];
170
+ ```
171
+
172
+ ## Benefits of Config-Driven Architecture
173
+
174
+ 1. **Clean Separation**: Tool definitions are separate from server setup
175
+ 2. **Type Safety**: Full TypeScript support for schemas and handlers
176
+ 3. **Documentation**: Config serves as living documentation
177
+ 4. **Testing**: Easier to unit test individual tools
178
+ 5. **Extensibility**: Simple to add new tool categories
179
+ 6. **Maintainability**: Consistent patterns across all tools
180
+ 7. **Scalability**: Easy to manage many tools without cluttering main file
181
+
182
+ ## Server Registration Pattern
183
+
184
+ The server automatically registers all tools from configuration:
185
+
186
+ ```typescript
187
+ // Automatic registration from configs - no manual server.tool() calls needed
188
+ allToolConfigs.forEach((toolConfig) => {
189
+ server.tool(
190
+ toolConfig.name, // Tool name from config
191
+ toolConfig.description, // Description from config
192
+ toolConfig.schema, // Zod schema from config
193
+ async (params: any) => { // Handler wrapper
194
+ return toolConfig.handler(params, {
195
+ authManager,
196
+ getYouTubeClient,
197
+ clearYouTubeClientCache
198
+ });
199
+ }
200
+ );
201
+ });
202
+ ```
203
+
204
+ ## Error Handling
205
+
206
+ All tools follow a consistent error handling pattern:
207
+
208
+ ```typescript
209
+ try {
210
+ // Tool implementation
211
+ return {
212
+ content: [{ type: "text", text: "Success result" }]
213
+ };
214
+ } catch (error) {
215
+ return {
216
+ content: [{
217
+ type: "text",
218
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
219
+ }],
220
+ isError: true
221
+ };
222
+ }
223
+ ```
224
+
225
+ ## Context Injection
226
+
227
+ Tools receive a context object with shared dependencies:
228
+
229
+ ```typescript
230
+ interface ToolContext {
231
+ authManager: AuthManager;
232
+ getYouTubeClient: () => Promise<YouTubeClient>;
233
+ clearYouTubeClientCache: () => void;
234
+ }
235
+ ```
236
+
237
+ This architecture makes the codebase more maintainable, scalable, and easier to extend while preserving all existing functionality.
@@ -0,0 +1,176 @@
1
+ import { authenticate } from '@google-cloud/local-auth';
2
+ import { promises as fs } from 'fs';
3
+ import { OAuth2Client } from 'google-auth-library';
4
+ import path from 'path';
5
+ import { AuthenticationError, TokenExpiredError } from './types.js';
6
+ export class AuthManager {
7
+ AUTH_DIR = path.join(process.cwd(), 'src', 'auth');
8
+ CREDENTIALS_PATH = path.join(this.AUTH_DIR, 'credentials.json');
9
+ TOKEN_PATH = path.join(this.AUTH_DIR, 'token.json');
10
+ SCOPES = [
11
+ 'https://www.googleapis.com/auth/youtube.readonly',
12
+ 'https://www.googleapis.com/auth/yt-analytics.readonly',
13
+ 'https://www.googleapis.com/auth/youtubepartner'
14
+ ];
15
+ authClient = null;
16
+ constructor() {
17
+ }
18
+ async getAuthClient() {
19
+ // Return cached client if available and valid
20
+ if (this.authClient) {
21
+ try {
22
+ await this.refreshTokenIfNeeded(this.authClient);
23
+ return this.authClient;
24
+ }
25
+ catch (error) {
26
+ console.log('Cached auth client invalid, creating new one...');
27
+ this.authClient = null;
28
+ }
29
+ }
30
+ try {
31
+ // Try to load existing token
32
+ const content = await fs.readFile(this.TOKEN_PATH, 'utf8');
33
+ const tokenData = JSON.parse(content);
34
+ // Load credentials to get client_id and client_secret
35
+ const credentialsContent = await fs.readFile(this.CREDENTIALS_PATH, 'utf8');
36
+ const credentials = JSON.parse(credentialsContent);
37
+ // Create OAuth2Client with proper credentials
38
+ this.authClient = new OAuth2Client(credentials.web.client_id, credentials.web.client_secret, credentials.web.redirect_uris[0]);
39
+ // Set the stored tokens
40
+ this.authClient.setCredentials({
41
+ access_token: tokenData.access_token,
42
+ refresh_token: tokenData.refresh_token,
43
+ expiry_date: tokenData.expiry_date
44
+ });
45
+ // Refresh token if needed
46
+ await this.refreshTokenIfNeeded(this.authClient);
47
+ return this.authClient;
48
+ }
49
+ catch (error) {
50
+ // If no token exists or loading fails, trigger authentication
51
+ console.log('No valid token found, initiating authentication flow...');
52
+ this.authClient = null;
53
+ return await this.authenticate();
54
+ }
55
+ }
56
+ async authenticate() {
57
+ try {
58
+ // Check if credentials file exists
59
+ try {
60
+ await fs.access(this.CREDENTIALS_PATH);
61
+ }
62
+ catch {
63
+ throw new AuthenticationError(`Credentials file not found at ${this.CREDENTIALS_PATH}. ` +
64
+ 'Please place your Google OAuth credentials in src/auth/credentials.json');
65
+ }
66
+ console.log('Auth manager CREDENTIALS_PATH', this.CREDENTIALS_PATH);
67
+ // Trigger OAuth flow using local-auth
68
+ const client = await authenticate({
69
+ scopes: this.SCOPES,
70
+ keyfilePath: this.CREDENTIALS_PATH,
71
+ });
72
+ // Save tokens
73
+ if (client.credentials) {
74
+ this.authClient = client;
75
+ await this.saveToken(this.authClient);
76
+ console.log('Authentication successful! Tokens saved.');
77
+ }
78
+ return this.authClient || client;
79
+ }
80
+ catch (error) {
81
+ if (error instanceof AuthenticationError) {
82
+ throw error;
83
+ }
84
+ throw new AuthenticationError(`Authentication failed: ${error}`);
85
+ }
86
+ }
87
+ async refreshTokenIfNeeded(auth) {
88
+ try {
89
+ // Check if token is expired or about to expire (within 5 minutes)
90
+ const now = Date.now();
91
+ const expiryDate = auth.credentials.expiry_date;
92
+ const fiveMinutesFromNow = now + (5 * 60 * 1000);
93
+ if (!expiryDate || expiryDate <= fiveMinutesFromNow) {
94
+ console.log('Token expired or expiring soon, refreshing...');
95
+ // Ensure we have a refresh token
96
+ if (!auth.credentials.refresh_token) {
97
+ console.error('No refresh token available');
98
+ throw new TokenExpiredError('No refresh token available');
99
+ }
100
+ const { credentials } = await auth.refreshAccessToken();
101
+ auth.setCredentials(credentials);
102
+ // Update stored token with new access token
103
+ await this.updateStoredToken(auth);
104
+ console.log('Token refreshed successfully');
105
+ }
106
+ }
107
+ catch (error) {
108
+ console.error('Token refresh failed:', error);
109
+ // Clear the cached client so we don't keep using invalid tokens
110
+ this.authClient = null;
111
+ throw new TokenExpiredError('Token refresh failed - please re-authenticate');
112
+ }
113
+ }
114
+ async saveToken(client) {
115
+ try {
116
+ // Read credentials to get client_id and client_secret
117
+ const credentialsContent = await fs.readFile(this.CREDENTIALS_PATH, 'utf8');
118
+ const credentials = JSON.parse(credentialsContent);
119
+ const tokenData = {
120
+ type: 'authorized_user',
121
+ client_id: credentials.web.client_id,
122
+ client_secret: credentials.web.client_secret,
123
+ refresh_token: client.credentials.refresh_token,
124
+ access_token: client.credentials.access_token || undefined,
125
+ expiry_date: client.credentials.expiry_date || undefined
126
+ };
127
+ await fs.writeFile(this.TOKEN_PATH, JSON.stringify(tokenData, null, 2));
128
+ // Set restrictive permissions on token file
129
+ await fs.chmod(this.TOKEN_PATH, 0o600);
130
+ }
131
+ catch (error) {
132
+ throw new AuthenticationError(`Failed to save token: ${error}`);
133
+ }
134
+ }
135
+ async updateStoredToken(auth) {
136
+ try {
137
+ const content = await fs.readFile(this.TOKEN_PATH, 'utf8');
138
+ const tokenData = JSON.parse(content);
139
+ // Update with new access token and expiry
140
+ tokenData.access_token = auth.credentials.access_token || undefined;
141
+ tokenData.expiry_date = auth.credentials.expiry_date || undefined;
142
+ await fs.writeFile(this.TOKEN_PATH, JSON.stringify(tokenData, null, 2));
143
+ }
144
+ catch (error) {
145
+ throw new AuthenticationError(`Failed to update stored token: ${error}`);
146
+ }
147
+ }
148
+ async revokeToken() {
149
+ try {
150
+ const auth = await this.getAuthClient();
151
+ await auth.revokeCredentials();
152
+ // Clear cached client
153
+ this.authClient = null;
154
+ // Remove stored token file
155
+ try {
156
+ await fs.unlink(this.TOKEN_PATH);
157
+ console.log('Token revoked and removed successfully');
158
+ }
159
+ catch (error) {
160
+ console.warn('Failed to remove token file:', error);
161
+ }
162
+ }
163
+ catch (error) {
164
+ throw new AuthenticationError(`Failed to revoke token: ${error}`);
165
+ }
166
+ }
167
+ async isAuthenticated() {
168
+ try {
169
+ const auth = await this.getAuthClient();
170
+ return !!auth.credentials.access_token;
171
+ }
172
+ catch {
173
+ return false;
174
+ }
175
+ }
176
+ }
@@ -0,0 +1,57 @@
1
+ import { z } from "zod";
2
+ export const authTools = [
3
+ {
4
+ name: "check_auth_status",
5
+ description: "Check if the user is authenticated with YouTube",
6
+ category: "authentication",
7
+ schema: z.object({}),
8
+ handler: async (_, { authManager }) => {
9
+ try {
10
+ const isAuthenticated = await authManager.isAuthenticated();
11
+ return {
12
+ content: [{
13
+ type: "text",
14
+ text: `Authentication Status: ${isAuthenticated ? 'Authenticated' : 'Not Authenticated'}`
15
+ }]
16
+ };
17
+ }
18
+ catch (error) {
19
+ return {
20
+ content: [{
21
+ type: "text",
22
+ text: `Error checking auth status: ${error instanceof Error ? error.message : String(error)}`
23
+ }],
24
+ isError: true
25
+ };
26
+ }
27
+ },
28
+ },
29
+ {
30
+ name: "revoke_auth",
31
+ description: "Revoke YouTube authentication and remove stored tokens",
32
+ category: "authentication",
33
+ schema: z.object({}),
34
+ handler: async (_, { authManager, clearYouTubeClientCache }) => {
35
+ try {
36
+ await authManager.revokeToken();
37
+ // Clear YouTube client cache
38
+ clearYouTubeClientCache();
39
+ return {
40
+ content: [{
41
+ type: "text",
42
+ text: "Authentication revoked successfully. You will need to re-authenticate to use YouTube tools."
43
+ }]
44
+ };
45
+ }
46
+ catch (error) {
47
+ return {
48
+ content: [{
49
+ type: "text",
50
+ text: `Error revoking auth: ${error instanceof Error ? error.message : String(error)}`
51
+ }],
52
+ isError: true
53
+ };
54
+ }
55
+ },
56
+ },
57
+ ];
@@ -0,0 +1,27 @@
1
+ // Error types
2
+ export class AuthenticationError extends Error {
3
+ account;
4
+ constructor(message, account) {
5
+ super(message);
6
+ this.account = account;
7
+ this.name = 'AuthenticationError';
8
+ }
9
+ }
10
+ export class TokenExpiredError extends AuthenticationError {
11
+ constructor(account) {
12
+ super(`Token expired for account ${account}`, account);
13
+ this.name = 'TokenExpiredError';
14
+ }
15
+ }
16
+ export class QuotaExceededError extends Error {
17
+ constructor(quotaType) {
18
+ super(`YouTube API quota exceeded: ${quotaType}`);
19
+ this.name = 'QuotaExceededError';
20
+ }
21
+ }
22
+ export class RateLimitError extends Error {
23
+ constructor(message) {
24
+ super(message);
25
+ this.name = 'RateLimitError';
26
+ }
27
+ }
package/build/index.js ADDED
@@ -0,0 +1,115 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { AuthManager } from './auth/auth-manager.js';
4
+ import { AuthenticationError } from './auth/types.js';
5
+ import { allTools } from './tool-configs.js';
6
+ import { allPrompts } from './prompt-configs.js';
7
+ import { YouTubeClient } from './youtube/youtube-client.js';
8
+ // Create server instance
9
+ const server = new McpServer({
10
+ name: "youtube-analytics-mcp",
11
+ version: "1.0.0",
12
+ capabilities: {
13
+ resources: {},
14
+ tools: {},
15
+ prompts: {},
16
+ },
17
+ });
18
+ // Initialize auth manager
19
+ const authManager = new AuthManager();
20
+ // Cache for YouTube client
21
+ let youtubeClientCache = null;
22
+ // Helper function to get YouTube client
23
+ async function getYouTubeClient() {
24
+ try {
25
+ // Return cached client if available
26
+ if (youtubeClientCache) {
27
+ return youtubeClientCache;
28
+ }
29
+ const auth = await authManager.getAuthClient();
30
+ youtubeClientCache = new YouTubeClient(auth);
31
+ return youtubeClientCache;
32
+ }
33
+ catch (error) {
34
+ // Clear cache on error
35
+ youtubeClientCache = null;
36
+ if (error instanceof AuthenticationError) {
37
+ throw new Error(`Authentication failed: ${error.message}`);
38
+ }
39
+ throw new Error(`Failed to get YouTube client: ${error}`);
40
+ }
41
+ }
42
+ // Helper function to clear YouTube client cache
43
+ function clearYouTubeClientCache() {
44
+ youtubeClientCache = null;
45
+ }
46
+ // Register all tools
47
+ allTools.forEach((toolConfig) => {
48
+ console.error(`Registering tool: ${toolConfig.name}`);
49
+ server.registerTool(toolConfig.name, {
50
+ description: toolConfig.description,
51
+ inputSchema: toolConfig.schema?.shape || {},
52
+ }, async (params) => {
53
+ try {
54
+ console.error(`Executing tool: ${toolConfig.name}`);
55
+ return await toolConfig.handler(params, {
56
+ authManager,
57
+ getYouTubeClient,
58
+ clearYouTubeClientCache
59
+ });
60
+ }
61
+ catch (error) {
62
+ console.error(`Error in tool ${toolConfig.name}:`, error);
63
+ return {
64
+ content: [{
65
+ type: "text",
66
+ text: `Error executing ${toolConfig.name}: ${error instanceof Error ? error.message : String(error)}`
67
+ }],
68
+ isError: true
69
+ };
70
+ }
71
+ });
72
+ });
73
+ console.error(`Total tools registered: ${allTools.length}`);
74
+ // Register all prompts
75
+ allPrompts.forEach((promptConfig) => {
76
+ console.error(`Registering prompt: ${promptConfig.name}`);
77
+ server.registerPrompt(promptConfig.name, {
78
+ title: promptConfig.title,
79
+ description: promptConfig.description,
80
+ argsSchema: promptConfig.argsSchema,
81
+ }, async (args) => {
82
+ try {
83
+ console.error(`Executing prompt: ${promptConfig.name}`);
84
+ return await promptConfig.handler(args);
85
+ }
86
+ catch (error) {
87
+ console.error(`Error in prompt ${promptConfig.name}:`, error);
88
+ return {
89
+ content: [{
90
+ type: "text",
91
+ text: `Error executing ${promptConfig.name}: ${error instanceof Error ? error.message : String(error)}`
92
+ }],
93
+ isError: true
94
+ };
95
+ }
96
+ });
97
+ });
98
+ console.error(`Total prompts registered: ${allPrompts.length}`);
99
+ async function main() {
100
+ const transport = new StdioServerTransport();
101
+ await server.connect(transport);
102
+ console.error("YouTube Analytics MCP Server running on stdio");
103
+ }
104
+ process.on('SIGINT', async () => {
105
+ console.error("Shutting down server...");
106
+ process.exit(0);
107
+ });
108
+ process.on('SIGTERM', async () => {
109
+ console.error("Shutting down server...");
110
+ process.exit(0);
111
+ });
112
+ main().catch((error) => {
113
+ console.error("Fatal error in main():", error);
114
+ process.exit(1);
115
+ });