@dalcontak/blogger-mcp-server 1.0.0 → 1.0.2

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/dist/index.js CHANGED
@@ -6,13 +6,9 @@ const server_1 = require("./server");
6
6
  const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
7
7
  const http_1 = require("http");
8
8
  const ui_manager_1 = require("./ui-manager");
9
- /**
10
- * Main entry point for the Blogger MCP server
11
- */
12
9
  async function main() {
13
10
  try {
14
11
  console.log('Starting Blogger MCP server...');
15
- // Verify that at least one authentication method is configured
16
12
  const hasOAuth2 = !!(config_1.config.oauth2.clientId && config_1.config.oauth2.clientSecret && config_1.config.oauth2.refreshToken);
17
13
  const hasApiKey = !!config_1.config.blogger.apiKey;
18
14
  if (!hasOAuth2 && !hasApiKey) {
@@ -21,15 +17,8 @@ async function main() {
21
17
  'GOOGLE_CLIENT_ID + GOOGLE_CLIENT_SECRET + GOOGLE_REFRESH_TOKEN (full access).');
22
18
  process.exit(1);
23
19
  }
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
20
+ console.log(`Authentication mode: ${hasOAuth2 ? 'OAuth2 (full access)' : 'API Key (read-only)'}`);
31
21
  const bloggerService = new bloggerService_1.BloggerService();
32
- // Convert configuration to the format expected by the server
33
22
  const serverMode = config_1.config.mode === 'http'
34
23
  ? { type: 'http', host: config_1.config.http.host, port: config_1.config.http.port }
35
24
  : { type: 'stdio' };
@@ -39,24 +28,17 @@ async function main() {
39
28
  oauth2: config_1.config.oauth2,
40
29
  logging: config_1.config.logging
41
30
  };
42
- // Initialize the MCP server with all tools
43
31
  const server = (0, server_1.initMCPServer)(bloggerService, serverConfig);
44
- // Get tool definitions for direct access in HTTP mode and stats
45
32
  const toolDefinitions = (0, server_1.createToolDefinitions)(bloggerService);
46
33
  const toolMap = new Map(toolDefinitions.map(t => [t.name, t]));
47
34
  const serverTools = toolDefinitions.map(t => t.name);
48
- // Initialize the Web UI only if UI_PORT is set
49
35
  let uiManager;
50
36
  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
- }
37
+ if (config_1.config.ui.port > 0 && config_1.config.ui.port < 65536) {
38
+ uiManager = new ui_manager_1.WebUIManager();
39
+ uiPort = config_1.config.ui.port;
40
+ await uiManager.start(uiPort);
58
41
  }
59
- // Initialize server statistics and status
60
42
  let serverStatus = {
61
43
  running: true,
62
44
  mode: serverMode.type,
@@ -64,25 +46,77 @@ async function main() {
64
46
  connections: 0,
65
47
  tools: serverTools
66
48
  };
67
- const serverStats = {
49
+ const connections = {};
50
+ const stats = {
68
51
  totalRequests: 0,
69
52
  successfulRequests: 0,
70
- failedRequests: 0,
71
- averageResponseTime: 0,
53
+ totalResponseTime: 0,
72
54
  toolUsage: serverTools.reduce((acc, tool) => {
73
55
  acc[tool] = 0;
74
56
  return acc;
75
57
  }, {})
76
58
  };
59
+ function updateStats(tool, success = true, duration = 0) {
60
+ stats.totalRequests++;
61
+ if (success) {
62
+ stats.successfulRequests++;
63
+ stats.totalResponseTime += duration;
64
+ }
65
+ if (stats.toolUsage[tool] !== undefined) {
66
+ stats.toolUsage[tool]++;
67
+ }
68
+ const updatedStats = {
69
+ totalRequests: stats.totalRequests,
70
+ successfulRequests: stats.successfulRequests,
71
+ failedRequests: stats.totalRequests - stats.successfulRequests,
72
+ averageResponseTime: stats.successfulRequests > 0
73
+ ? Math.round(stats.totalResponseTime / stats.successfulRequests)
74
+ : 0,
75
+ toolUsage: stats.toolUsage
76
+ };
77
+ uiManager?.updateStats(updatedStats);
78
+ }
79
+ function updateConnections(clientId, clientIp) {
80
+ const now = new Date();
81
+ if (!connections[clientId]) {
82
+ connections[clientId] = {
83
+ id: clientId,
84
+ ip: clientIp,
85
+ connectedAt: now,
86
+ lastActivity: now,
87
+ requestCount: 1
88
+ };
89
+ }
90
+ else {
91
+ connections[clientId].lastActivity = now;
92
+ connections[clientId].requestCount++;
93
+ }
94
+ const fiveMinutesAgo = new Date(now.getTime() - 5 * 60 * 1000);
95
+ Object.keys(connections).forEach(id => {
96
+ if (connections[id].lastActivity < fiveMinutesAgo) {
97
+ delete connections[id];
98
+ }
99
+ });
100
+ uiManager?.updateConnections(Object.values(connections));
101
+ serverStatus = {
102
+ ...serverStatus,
103
+ connections: Object.keys(connections).length
104
+ };
105
+ uiManager?.updateStatus(serverStatus);
106
+ }
77
107
  if (uiManager) {
108
+ const initialStats = {
109
+ totalRequests: 0,
110
+ successfulRequests: 0,
111
+ failedRequests: 0,
112
+ averageResponseTime: 0,
113
+ toolUsage: stats.toolUsage
114
+ };
78
115
  uiManager.updateStatus(serverStatus);
79
- uiManager.updateStats(serverStats);
116
+ uiManager.updateStats(initialStats);
80
117
  }
81
- // Configure the appropriate transport based on the mode
82
118
  let httpServer;
83
119
  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
120
  const httpMode = serverMode;
87
121
  httpServer = new http_1.Server((req, res) => {
88
122
  if (req.method === 'OPTIONS') {
@@ -101,13 +135,13 @@ async function main() {
101
135
  }
102
136
  let body = '';
103
137
  let bodySize = 0;
104
- const MAX_BODY_SIZE = 1024 * 1024; // 1MB limit
138
+ const MAX_BODY_SIZE = 1024 * 1024;
105
139
  req.on('data', chunk => {
106
140
  bodySize += chunk.length;
107
141
  if (bodySize > MAX_BODY_SIZE) {
108
142
  res.writeHead(413, { 'Content-Type': 'application/json' });
109
143
  res.end(JSON.stringify({ error: 'Request entity too large' }));
110
- req.destroy(); // Stop receiving data
144
+ req.destroy();
111
145
  return;
112
146
  }
113
147
  body += chunk.toString();
@@ -118,73 +152,40 @@ async function main() {
118
152
  try {
119
153
  const request = JSON.parse(body);
120
154
  const { tool, params } = request;
121
- // Add client connection
122
155
  const clientIp = req.socket.remoteAddress || 'unknown';
123
156
  updateConnections(req.socket.remotePort?.toString() || 'client', clientIp);
124
- // Call the appropriate tool
125
157
  try {
126
158
  const startTime = Date.now();
127
159
  const toolDef = toolMap.get(tool);
128
160
  if (!toolDef) {
129
161
  throw new Error(`Unknown tool: ${tool}`);
130
162
  }
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
163
+ const validatedParams = toolDef.args.parse(params || {});
140
164
  const result = await toolDef.handler(validatedParams);
141
165
  const duration = Date.now() - startTime;
142
- // Update success statistics
143
166
  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
167
  res.writeHead(200, {
152
168
  'Content-Type': 'application/json',
153
169
  'Access-Control-Allow-Origin': '*'
154
170
  });
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));
171
+ const textContent = result.content[0]?.text;
172
+ if (textContent) {
173
+ try {
174
+ const parsedContent = JSON.parse(textContent);
175
+ res.end(JSON.stringify(parsedContent));
176
+ }
177
+ catch {
178
+ res.end(JSON.stringify(result));
179
+ }
175
180
  }
176
- catch (e) {
177
- // If not JSON, return as is wrapped
181
+ else {
178
182
  res.end(JSON.stringify(result));
179
183
  }
180
184
  }
181
185
  catch (error) {
182
- // Update failure statistics
183
186
  updateStats(tool, false);
184
187
  res.writeHead(400, { 'Content-Type': 'application/json' });
185
- res.end(JSON.stringify({
186
- error: `Error executing tool: ${error}`
187
- }));
188
+ res.end(JSON.stringify({ error: `Error executing tool: ${error}` }));
188
189
  }
189
190
  }
190
191
  catch (error) {
@@ -202,94 +203,20 @@ async function main() {
202
203
  });
203
204
  }
204
205
  else {
205
- // For stdio mode, we use the official MCP SDK transport
206
206
  const transport = new stdio_js_1.StdioServerTransport();
207
207
  await server.connect(transport);
208
- console.log(`Blogger MCP server started in stdio mode`);
208
+ console.log('Blogger MCP server started in stdio mode');
209
209
  if (uiPort) {
210
210
  console.log(`Web UI available at http://localhost:${uiPort}`);
211
211
  }
212
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
213
  const shutdown = async () => {
284
214
  console.log('Shutting down...');
285
215
  serverStatus = { ...serverStatus, running: false };
286
- if (uiManager) {
287
- uiManager.updateStatus(serverStatus);
288
- }
216
+ uiManager?.updateStatus(serverStatus);
289
217
  if (httpServer) {
290
218
  httpServer.close();
291
219
  }
292
- // Allow time for cleanup if needed
293
220
  setTimeout(() => process.exit(0), 1000);
294
221
  };
295
222
  process.on('SIGINT', shutdown);
@@ -300,5 +227,4 @@ async function main() {
300
227
  process.exit(1);
301
228
  }
302
229
  }
303
- // Run main function
304
230
  main();
package/dist/server.d.ts CHANGED
@@ -1,16 +1,5 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { ServerConfig, ToolDefinition } from './types';
3
3
  import { BloggerService } from './bloggerService';
4
- /**
5
- * Creates the tool definitions for the Blogger MCP server
6
- * @param bloggerService Blogger service to interact with the API
7
- * @returns Array of tool definitions
8
- */
9
4
  export declare function createToolDefinitions(bloggerService: BloggerService): ToolDefinition[];
10
- /**
11
- * Initializes the MCP server with all Blogger tools
12
- * @param bloggerService Blogger service to interact with the API
13
- * @param config Server configuration
14
- * @returns MCP server instance
15
- */
16
5
  export declare function initMCPServer(bloggerService: BloggerService, config: ServerConfig): McpServer;