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.
- package/README.md +237 -0
- package/build/auth/auth-manager.js +176 -0
- package/build/auth/tool-configs.js +57 -0
- package/build/auth/types.js +27 -0
- package/build/index.js +115 -0
- package/build/prompt-configs.js +145 -0
- package/build/server/info-configs.js +42 -0
- package/build/tool-configs.js +18 -0
- package/build/types.js +27 -0
- package/build/utils/formatters/audience.js +122 -0
- package/build/utils/formatters/channel.js +120 -0
- package/build/utils/formatters/discovery.js +165 -0
- package/build/utils/formatters/engagement.js +126 -0
- package/build/utils/formatters/health.js +155 -0
- package/build/utils/formatters/index.js +6 -0
- package/build/utils/formatters/performance.js +181 -0
- package/build/utils/index.js +3 -0
- package/build/utils/parsers/analytics.js +100 -0
- package/build/utils/parsers/index.js +1 -0
- package/build/utils/transformers/analytics.js +53 -0
- package/build/utils/transformers/index.js +3 -0
- package/build/utils/transformers/statistics.js +37 -0
- package/build/utils/transformers/thumbnails.js +26 -0
- package/build/youtube/tools/audience-configs.js +165 -0
- package/build/youtube/tools/channel-configs.js +113 -0
- package/build/youtube/tools/discovery-configs.js +109 -0
- package/build/youtube/tools/engagement-configs.js +94 -0
- package/build/youtube/tools/health-configs.js +314 -0
- package/build/youtube/tools/performance-configs.js +250 -0
- package/build/youtube/types.js +13 -0
- package/build/youtube/youtube-client.js +579 -0
- 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
|
+
});
|