@dalcontak/blogger-mcp-server 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.
@@ -0,0 +1,323 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BloggerService = void 0;
4
+ const googleapis_1 = require("googleapis");
5
+ const config_1 = require("./config");
6
+ /**
7
+ * Google Blogger API interaction service
8
+ *
9
+ * Supports two authentication modes:
10
+ * - OAuth2 (GOOGLE_CLIENT_ID + GOOGLE_CLIENT_SECRET + GOOGLE_REFRESH_TOKEN):
11
+ * full access (read + write). Required for listBlogs, createPost, updatePost, deletePost.
12
+ * - API Key (BLOGGER_API_KEY): read-only access to public blogs.
13
+ * Works for getBlog, listPosts, getPost, searchPosts, listLabels, getLabel.
14
+ *
15
+ * If both are configured, OAuth2 is used (it covers all operations).
16
+ */
17
+ class BloggerService {
18
+ /**
19
+ * Initializes the Blogger service with OAuth2 or API key
20
+ */
21
+ constructor() {
22
+ const { oauth2 } = config_1.config;
23
+ const hasOAuth2 = !!(oauth2.clientId && oauth2.clientSecret && oauth2.refreshToken);
24
+ if (hasOAuth2) {
25
+ const oauth2Client = new googleapis_1.google.auth.OAuth2(oauth2.clientId, oauth2.clientSecret);
26
+ oauth2Client.setCredentials({ refresh_token: oauth2.refreshToken });
27
+ this.blogger = googleapis_1.google.blogger({
28
+ version: 'v3',
29
+ auth: oauth2Client,
30
+ timeout: config_1.config.blogger.timeout
31
+ });
32
+ this.isOAuth2 = true;
33
+ console.log('BloggerService initialized with OAuth2 (full access)');
34
+ }
35
+ else if (config_1.config.blogger.apiKey) {
36
+ this.blogger = googleapis_1.google.blogger({
37
+ version: 'v3',
38
+ auth: config_1.config.blogger.apiKey,
39
+ timeout: config_1.config.blogger.timeout
40
+ });
41
+ this.isOAuth2 = false;
42
+ console.log('BloggerService initialized with API Key (read-only)');
43
+ }
44
+ else {
45
+ throw new Error('No authentication configured. ' +
46
+ 'Set BLOGGER_API_KEY (read-only) or ' +
47
+ 'GOOGLE_CLIENT_ID + GOOGLE_CLIENT_SECRET + GOOGLE_REFRESH_TOKEN (full access).');
48
+ }
49
+ }
50
+ /**
51
+ * Checks that OAuth2 authentication is available.
52
+ * Throws an explicit error if the operation requires OAuth2 and we are in API key mode.
53
+ */
54
+ requireOAuth2(operation) {
55
+ if (!this.isOAuth2) {
56
+ throw new Error(`Operation "${operation}" requires OAuth2 authentication. ` +
57
+ 'API Key mode only allows reading public blogs. ' +
58
+ 'Configure GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET and GOOGLE_REFRESH_TOKEN.');
59
+ }
60
+ }
61
+ /**
62
+ * Lists all blogs for the authenticated user.
63
+ * Requires OAuth2 (blogs.listByUser with userId: 'self').
64
+ * @returns Blog list
65
+ */
66
+ async listBlogs() {
67
+ this.requireOAuth2('list_blogs');
68
+ try {
69
+ const response = await this.blogger.blogs.listByUser({
70
+ userId: 'self'
71
+ });
72
+ return response.data;
73
+ }
74
+ catch (error) {
75
+ console.error('Error fetching blogs:', error);
76
+ throw error;
77
+ }
78
+ }
79
+ /**
80
+ * Retrieves details of a specific blog
81
+ * @param blogId ID of the blog to retrieve
82
+ * @returns Blog details
83
+ */
84
+ async getBlog(blogId) {
85
+ try {
86
+ const response = await this.blogger.blogs.get({
87
+ blogId
88
+ });
89
+ return response.data;
90
+ }
91
+ catch (error) {
92
+ console.error(`Error fetching blog ${blogId}:`, error);
93
+ throw error;
94
+ }
95
+ }
96
+ /**
97
+ * Retrieves a blog by its URL
98
+ * @param url Blog URL
99
+ * @returns Blog details
100
+ */
101
+ async getBlogByUrl(url) {
102
+ try {
103
+ const response = await this.blogger.blogs.getByUrl({
104
+ url
105
+ });
106
+ return response.data;
107
+ }
108
+ catch (error) {
109
+ console.error(`Error fetching blog by URL ${url}:`, error);
110
+ throw error;
111
+ }
112
+ }
113
+ /**
114
+ * Simulates blog creation.
115
+ * Note: The Blogger API does not actually allow creating a blog via API.
116
+ * This method simulates the functionality and returns an explanatory error message.
117
+ *
118
+ * @param blogData Blog data to create
119
+ * @returns Explanatory error message
120
+ */
121
+ async createBlog(blogData) {
122
+ // Simulate a delay to make the response more realistic
123
+ await new Promise(resolve => setTimeout(resolve, 500));
124
+ // Return an explanatory error message
125
+ return {
126
+ error: true,
127
+ message: "The Google Blogger API does not allow creating a new blog via API. Please create a blog manually on blogger.com.",
128
+ details: "This limitation is documented by Google. Blogs must be created via the Blogger web interface.",
129
+ suggestedAction: "Create a blog at https://www.blogger.com, then use its ID with this MCP server."
130
+ };
131
+ }
132
+ /**
133
+ * Lists posts from a blog
134
+ * @param blogId Blog ID
135
+ * @param maxResults Maximum number of results to return
136
+ * @returns Post list
137
+ */
138
+ async listPosts(blogId, maxResults) {
139
+ try {
140
+ const response = await this.blogger.posts.list({
141
+ blogId,
142
+ maxResults: maxResults || config_1.config.blogger.maxResults
143
+ });
144
+ return response.data;
145
+ }
146
+ catch (error) {
147
+ console.error(`Error fetching posts for blog ${blogId}:`, error);
148
+ throw error;
149
+ }
150
+ }
151
+ /**
152
+ * Searches posts in a blog using the native posts.search endpoint of the Blogger API
153
+ * @param blogId Blog ID
154
+ * @param query Search term
155
+ * @param maxResults Maximum number of results to return
156
+ * @returns List of matching posts
157
+ */
158
+ async searchPosts(blogId, query, maxResults) {
159
+ try {
160
+ const response = await this.blogger.posts.search({
161
+ blogId,
162
+ q: query,
163
+ fetchBodies: true
164
+ });
165
+ // The search endpoint does not support maxResults directly,
166
+ // so we truncate client-side if needed
167
+ const items = response.data.items || [];
168
+ const limit = maxResults || config_1.config.blogger.maxResults;
169
+ return {
170
+ kind: response.data.kind,
171
+ items: items.slice(0, limit)
172
+ };
173
+ }
174
+ catch (error) {
175
+ console.error(`Error searching posts in blog ${blogId}:`, error);
176
+ throw error;
177
+ }
178
+ }
179
+ /**
180
+ * Retrieves a specific post
181
+ * @param blogId Blog ID
182
+ * @param postId Post ID
183
+ * @returns Post details
184
+ */
185
+ async getPost(blogId, postId) {
186
+ try {
187
+ const response = await this.blogger.posts.get({
188
+ blogId,
189
+ postId
190
+ });
191
+ return response.data;
192
+ }
193
+ catch (error) {
194
+ console.error(`Error fetching post ${postId}:`, error);
195
+ throw error;
196
+ }
197
+ }
198
+ /**
199
+ * Creates a new post in a blog.
200
+ * Requires OAuth2.
201
+ * @param blogId Blog ID
202
+ * @param postData Post data to create
203
+ * @returns Created post
204
+ */
205
+ async createPost(blogId, postData) {
206
+ this.requireOAuth2('create_post');
207
+ try {
208
+ const response = await this.blogger.posts.insert({
209
+ blogId,
210
+ requestBody: postData
211
+ });
212
+ return response.data;
213
+ }
214
+ catch (error) {
215
+ console.error(`Error creating post in blog ${blogId}:`, error);
216
+ throw error;
217
+ }
218
+ }
219
+ /**
220
+ * Updates an existing post.
221
+ * Requires OAuth2.
222
+ * @param blogId Blog ID
223
+ * @param postId Post ID
224
+ * @param postData Post data to update
225
+ * @returns Updated post
226
+ */
227
+ async updatePost(blogId, postId, postData) {
228
+ this.requireOAuth2('update_post');
229
+ try {
230
+ // Convert types to avoid compilation errors
231
+ const requestBody = {
232
+ title: postData.title,
233
+ content: postData.content,
234
+ labels: postData.labels
235
+ };
236
+ const response = await this.blogger.posts.update({
237
+ blogId,
238
+ postId,
239
+ requestBody
240
+ });
241
+ return response.data;
242
+ }
243
+ catch (error) {
244
+ console.error(`Error updating post ${postId}:`, error);
245
+ throw error;
246
+ }
247
+ }
248
+ /**
249
+ * Deletes a post.
250
+ * Requires OAuth2.
251
+ * @param blogId Blog ID
252
+ * @param postId Post ID
253
+ * @returns Deletion status
254
+ */
255
+ async deletePost(blogId, postId) {
256
+ this.requireOAuth2('delete_post');
257
+ try {
258
+ await this.blogger.posts.delete({
259
+ blogId,
260
+ postId
261
+ });
262
+ }
263
+ catch (error) {
264
+ console.error(`Error deleting post ${postId}:`, error);
265
+ throw error;
266
+ }
267
+ }
268
+ /**
269
+ * Lists labels from a blog
270
+ * @param blogId Blog ID
271
+ * @returns Label list
272
+ */
273
+ async listLabels(blogId) {
274
+ try {
275
+ // The Blogger API does not provide a direct endpoint to list labels
276
+ // We fetch all posts and extract unique labels
277
+ const response = await this.blogger.posts.list({
278
+ blogId,
279
+ maxResults: 50 // Fetch enough posts to extract labels
280
+ });
281
+ const posts = response.data.items || [];
282
+ const labelSet = new Set();
283
+ // Extract all unique labels from posts
284
+ posts.forEach(post => {
285
+ const postLabels = post.labels || [];
286
+ postLabels.forEach(label => labelSet.add(label));
287
+ });
288
+ // Convert to expected format
289
+ const labels = Array.from(labelSet).map(name => ({ name }));
290
+ return {
291
+ kind: 'blogger#labelList',
292
+ items: labels
293
+ };
294
+ }
295
+ catch (error) {
296
+ console.error(`Error fetching labels for blog ${blogId}:`, error);
297
+ throw error;
298
+ }
299
+ }
300
+ /**
301
+ * Retrieves a specific label
302
+ * @param blogId Blog ID
303
+ * @param labelName Label name
304
+ * @returns Label details
305
+ */
306
+ async getLabel(blogId, labelName) {
307
+ try {
308
+ // The Blogger API does not provide a direct endpoint to retrieve a label
309
+ // We check if the label exists by listing all labels
310
+ const labels = await this.listLabels(blogId);
311
+ const label = labels.items?.find(l => l.name === labelName);
312
+ if (!label) {
313
+ throw new Error(`Label ${labelName} not found`);
314
+ }
315
+ return label;
316
+ }
317
+ catch (error) {
318
+ console.error(`Error fetching label ${labelName}:`, error);
319
+ throw error;
320
+ }
321
+ }
322
+ }
323
+ exports.BloggerService = BloggerService;
@@ -0,0 +1,20 @@
1
+ export declare const config: {
2
+ mode: string;
3
+ http: {
4
+ host: string;
5
+ port: number;
6
+ };
7
+ blogger: {
8
+ apiKey: string | undefined;
9
+ maxResults: number;
10
+ timeout: number;
11
+ };
12
+ oauth2: {
13
+ clientId: string | undefined;
14
+ clientSecret: string | undefined;
15
+ refreshToken: string | undefined;
16
+ };
17
+ logging: {
18
+ level: string;
19
+ };
20
+ };
package/dist/config.js ADDED
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.config = void 0;
4
+ // MCP server configuration for Blogger
5
+ exports.config = {
6
+ // Server operating mode (stdio or http)
7
+ mode: process.env.MCP_MODE || 'stdio',
8
+ // HTTP mode configuration (if used)
9
+ http: {
10
+ host: process.env.MCP_HTTP_HOST || '0.0.0.0',
11
+ port: parseInt(process.env.MCP_HTTP_PORT || '3000', 10)
12
+ },
13
+ // Blogger API configuration
14
+ blogger: {
15
+ apiKey: process.env.BLOGGER_API_KEY,
16
+ // Default maximum number of results for list queries
17
+ maxResults: parseInt(process.env.BLOGGER_MAX_RESULTS || '10', 10),
18
+ // API request timeout in milliseconds
19
+ timeout: parseInt(process.env.BLOGGER_API_TIMEOUT || '30000', 10)
20
+ },
21
+ // OAuth2 configuration for authenticated operations (create, update, delete)
22
+ // If these variables are not set, the server runs in read-only mode (API key)
23
+ oauth2: {
24
+ clientId: process.env.GOOGLE_CLIENT_ID,
25
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET,
26
+ refreshToken: process.env.GOOGLE_REFRESH_TOKEN
27
+ },
28
+ // Logging configuration
29
+ logging: {
30
+ level: process.env.LOG_LEVEL || 'info'
31
+ }
32
+ };
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,304 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const config_1 = require("./config");
4
+ const bloggerService_1 = require("./bloggerService");
5
+ const server_1 = require("./server");
6
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
7
+ const http_1 = require("http");
8
+ const ui_manager_1 = require("./ui-manager");
9
+ /**
10
+ * Main entry point for the Blogger MCP server
11
+ */
12
+ async function main() {
13
+ try {
14
+ console.log('Starting Blogger MCP server...');
15
+ // Verify that at least one authentication method is configured
16
+ const hasOAuth2 = !!(config_1.config.oauth2.clientId && config_1.config.oauth2.clientSecret && config_1.config.oauth2.refreshToken);
17
+ const hasApiKey = !!config_1.config.blogger.apiKey;
18
+ if (!hasOAuth2 && !hasApiKey) {
19
+ console.error('ERROR: No authentication configured.\n' +
20
+ 'Set BLOGGER_API_KEY (read-only) or\n' +
21
+ 'GOOGLE_CLIENT_ID + GOOGLE_CLIENT_SECRET + GOOGLE_REFRESH_TOKEN (full access).');
22
+ process.exit(1);
23
+ }
24
+ if (hasOAuth2) {
25
+ console.log('Authentication mode: OAuth2 (full access)');
26
+ }
27
+ else {
28
+ console.log('Authentication mode: API Key (read-only)');
29
+ }
30
+ // Initialize the Blogger service
31
+ const bloggerService = new bloggerService_1.BloggerService();
32
+ // Convert configuration to the format expected by the server
33
+ const serverMode = config_1.config.mode === 'http'
34
+ ? { type: 'http', host: config_1.config.http.host, port: config_1.config.http.port }
35
+ : { type: 'stdio' };
36
+ const serverConfig = {
37
+ mode: serverMode,
38
+ blogger: config_1.config.blogger,
39
+ oauth2: config_1.config.oauth2,
40
+ logging: config_1.config.logging
41
+ };
42
+ // Initialize the MCP server with all tools
43
+ const server = (0, server_1.initMCPServer)(bloggerService, serverConfig);
44
+ // Get tool definitions for direct access in HTTP mode and stats
45
+ const toolDefinitions = (0, server_1.createToolDefinitions)(bloggerService);
46
+ const toolMap = new Map(toolDefinitions.map(t => [t.name, t]));
47
+ const serverTools = toolDefinitions.map(t => t.name);
48
+ // Initialize the Web UI only if UI_PORT is set
49
+ let uiManager;
50
+ let uiPort;
51
+ if (process.env.UI_PORT) {
52
+ const parsedPort = parseInt(process.env.UI_PORT);
53
+ if (!isNaN(parsedPort) && parsedPort > 0 && parsedPort < 65536) {
54
+ uiManager = new ui_manager_1.WebUIManager();
55
+ uiPort = parsedPort;
56
+ await uiManager.start(uiPort);
57
+ }
58
+ }
59
+ // Initialize server statistics and status
60
+ let serverStatus = {
61
+ running: true,
62
+ mode: serverMode.type,
63
+ startTime: new Date(),
64
+ connections: 0,
65
+ tools: serverTools
66
+ };
67
+ const serverStats = {
68
+ totalRequests: 0,
69
+ successfulRequests: 0,
70
+ failedRequests: 0,
71
+ averageResponseTime: 0,
72
+ toolUsage: serverTools.reduce((acc, tool) => {
73
+ acc[tool] = 0;
74
+ return acc;
75
+ }, {})
76
+ };
77
+ if (uiManager) {
78
+ uiManager.updateStatus(serverStatus);
79
+ uiManager.updateStats(serverStats);
80
+ }
81
+ // Configure the appropriate transport based on the mode
82
+ let httpServer;
83
+ if (serverMode.type === 'http') {
84
+ // For HTTP mode, we use Node.js HTTP server directly
85
+ // since the official MCP SDK does not have an HttpServerTransport equivalent
86
+ const httpMode = serverMode;
87
+ httpServer = new http_1.Server((req, res) => {
88
+ if (req.method === 'OPTIONS') {
89
+ res.writeHead(200, {
90
+ 'Access-Control-Allow-Origin': '*',
91
+ 'Access-Control-Allow-Methods': 'POST, OPTIONS',
92
+ 'Access-Control-Allow-Headers': 'Content-Type'
93
+ });
94
+ res.end();
95
+ return;
96
+ }
97
+ if (req.method !== 'POST') {
98
+ res.writeHead(405, { 'Content-Type': 'application/json' });
99
+ res.end(JSON.stringify({ error: 'Method not allowed' }));
100
+ return;
101
+ }
102
+ let body = '';
103
+ let bodySize = 0;
104
+ const MAX_BODY_SIZE = 1024 * 1024; // 1MB limit
105
+ req.on('data', chunk => {
106
+ bodySize += chunk.length;
107
+ if (bodySize > MAX_BODY_SIZE) {
108
+ res.writeHead(413, { 'Content-Type': 'application/json' });
109
+ res.end(JSON.stringify({ error: 'Request entity too large' }));
110
+ req.destroy(); // Stop receiving data
111
+ return;
112
+ }
113
+ body += chunk.toString();
114
+ });
115
+ req.on('end', async () => {
116
+ if (req.destroyed)
117
+ return;
118
+ try {
119
+ const request = JSON.parse(body);
120
+ const { tool, params } = request;
121
+ // Add client connection
122
+ const clientIp = req.socket.remoteAddress || 'unknown';
123
+ updateConnections(req.socket.remotePort?.toString() || 'client', clientIp);
124
+ // Call the appropriate tool
125
+ try {
126
+ const startTime = Date.now();
127
+ const toolDef = toolMap.get(tool);
128
+ if (!toolDef) {
129
+ throw new Error(`Unknown tool: ${tool}`);
130
+ }
131
+ // Validate parameters using Zod schema
132
+ let validatedParams;
133
+ try {
134
+ validatedParams = toolDef.args.parse(params || {});
135
+ }
136
+ catch (validationError) {
137
+ throw new Error(`Invalid parameters: ${validationError}`);
138
+ }
139
+ // Execute tool handler
140
+ const result = await toolDef.handler(validatedParams);
141
+ const duration = Date.now() - startTime;
142
+ // Update success statistics
143
+ updateStats(tool, true, duration);
144
+ // If the handler returned an isError: true, we might want to return 400 or just return the error object
145
+ // as per MCP protocol. Here we are in HTTP mode, let's just return 200 with the result object which contains the error message.
146
+ // But strictly speaking, if it's an error, we should probably update stats as failed?
147
+ // The handler catches exceptions and returns { isError: true, ... }.
148
+ // So if result.isError is true, we should count it as failed?
149
+ // The previous implementation counted catch block as failed.
150
+ // Let's stick to the previous logic: if handler throws, it's a failure. If handler returns result (even error result), it's success execution of the tool.
151
+ res.writeHead(200, {
152
+ 'Content-Type': 'application/json',
153
+ 'Access-Control-Allow-Origin': '*'
154
+ });
155
+ // MCP Tools return { content: [...] }, but the previous HTTP implementation returned simplified objects like { blogs: [...] }.
156
+ // To maintain backward compatibility with the previous HTTP API (if any clients rely on it),
157
+ // we might need to transform the MCP result format back to the simplified format?
158
+ // The previous switch statement returned `result = { blogs }`.
159
+ // The tool handlers now return `{ content: [{ type: 'text', text: JSON.stringify({ blogs }) }] }`.
160
+ // We should probably parse the JSON text back if we want to return JSON.
161
+ // OR, we just return the MCP result directly.
162
+ // Given that this is an MCP server, clients should expect MCP format.
163
+ // HOWEVER, the `index.ts` HTTP implementation seemed to be a custom JSON API wrapper around the tools.
164
+ // Let's try to parse the response text if possible to match previous behavior,
165
+ // OR better: accept that the response format changes to MCP standard or keep it simple.
166
+ // The previous implementation was: `res.end(JSON.stringify(result))` where result was `{ blogs: ... }`.
167
+ // The tool handlers return `{ content: [{ text: "{\"blogs\":...}" }] }`.
168
+ // Let's unwrap it for HTTP mode to keep it friendly, or just return the text.
169
+ // If we want to return pure JSON like before:
170
+ try {
171
+ const textContent = result.content[0].text;
172
+ // If the text is JSON, parse it and return that.
173
+ const parsedContent = JSON.parse(textContent);
174
+ res.end(JSON.stringify(parsedContent));
175
+ }
176
+ catch (e) {
177
+ // If not JSON, return as is wrapped
178
+ res.end(JSON.stringify(result));
179
+ }
180
+ }
181
+ catch (error) {
182
+ // Update failure statistics
183
+ updateStats(tool, false);
184
+ res.writeHead(400, { 'Content-Type': 'application/json' });
185
+ res.end(JSON.stringify({
186
+ error: `Error executing tool: ${error}`
187
+ }));
188
+ }
189
+ }
190
+ catch (error) {
191
+ res.writeHead(400, { 'Content-Type': 'application/json' });
192
+ res.end(JSON.stringify({ error: `Parsing error: ${error}` }));
193
+ }
194
+ });
195
+ });
196
+ httpServer.listen(httpMode.port, httpMode.host, () => {
197
+ console.log(`Blogger MCP server started in HTTP mode`);
198
+ console.log(`Listening on ${httpMode.host}:${httpMode.port}`);
199
+ if (uiPort) {
200
+ console.log(`Web UI available at http://localhost:${uiPort}`);
201
+ }
202
+ });
203
+ }
204
+ else {
205
+ // For stdio mode, we use the official MCP SDK transport
206
+ const transport = new stdio_js_1.StdioServerTransport();
207
+ await server.connect(transport);
208
+ console.log(`Blogger MCP server started in stdio mode`);
209
+ if (uiPort) {
210
+ console.log(`Web UI available at http://localhost:${uiPort}`);
211
+ }
212
+ }
213
+ // Functions to update statistics and connections
214
+ const connections = {};
215
+ let stats = {
216
+ totalRequests: 0,
217
+ successfulRequests: 0,
218
+ failedRequests: 0,
219
+ totalResponseTime: 0,
220
+ toolUsage: serverTools.reduce((acc, tool) => {
221
+ acc[tool] = 0;
222
+ return acc;
223
+ }, {})
224
+ };
225
+ function updateStats(tool, success = true, duration = 0) {
226
+ stats.totalRequests++;
227
+ if (success) {
228
+ stats.successfulRequests++;
229
+ stats.totalResponseTime += duration;
230
+ }
231
+ if (stats.toolUsage[tool] !== undefined) {
232
+ stats.toolUsage[tool]++;
233
+ }
234
+ const updatedStats = {
235
+ totalRequests: stats.totalRequests,
236
+ successfulRequests: stats.successfulRequests,
237
+ failedRequests: stats.totalRequests - stats.successfulRequests,
238
+ averageResponseTime: stats.successfulRequests > 0
239
+ ? Math.round(stats.totalResponseTime / stats.successfulRequests)
240
+ : 0,
241
+ toolUsage: stats.toolUsage
242
+ };
243
+ if (uiManager) {
244
+ uiManager.updateStats(updatedStats);
245
+ }
246
+ }
247
+ function updateConnections(clientId, clientIp) {
248
+ const now = new Date();
249
+ if (!connections[clientId]) {
250
+ connections[clientId] = {
251
+ id: clientId,
252
+ ip: clientIp,
253
+ connectedAt: now,
254
+ lastActivity: now,
255
+ requestCount: 1
256
+ };
257
+ }
258
+ else {
259
+ connections[clientId].lastActivity = now;
260
+ connections[clientId].requestCount++;
261
+ }
262
+ // Clean up inactive connections (older than 5 minutes)
263
+ const fiveMinutesAgo = new Date(now.getTime() - 5 * 60 * 1000);
264
+ Object.keys(connections).forEach(id => {
265
+ if (connections[id].lastActivity < fiveMinutesAgo) {
266
+ delete connections[id];
267
+ }
268
+ });
269
+ if (uiManager) {
270
+ uiManager.updateConnections(Object.values(connections));
271
+ }
272
+ // Update status with connection count
273
+ // FIX: Update the variable and then send it
274
+ serverStatus = {
275
+ ...serverStatus,
276
+ connections: Object.keys(connections).length
277
+ };
278
+ if (uiManager) {
279
+ uiManager.updateStatus(serverStatus);
280
+ }
281
+ }
282
+ // Graceful shutdown
283
+ const shutdown = async () => {
284
+ console.log('Shutting down...');
285
+ serverStatus = { ...serverStatus, running: false };
286
+ if (uiManager) {
287
+ uiManager.updateStatus(serverStatus);
288
+ }
289
+ if (httpServer) {
290
+ httpServer.close();
291
+ }
292
+ // Allow time for cleanup if needed
293
+ setTimeout(() => process.exit(0), 1000);
294
+ };
295
+ process.on('SIGINT', shutdown);
296
+ process.on('SIGTERM', shutdown);
297
+ }
298
+ catch (error) {
299
+ console.error('Error starting Blogger MCP server:', error);
300
+ process.exit(1);
301
+ }
302
+ }
303
+ // Run main function
304
+ main();