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