@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/.github/workflows/publish.yml +3 -0
- package/AGENTS.md +2 -2
- package/README.md +201 -100
- 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.js +80 -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 +5 -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 +115 -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/mcp-sdk-mock.d.ts +0 -57
- package/dist/mcp-sdk-mock.js +0 -227
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
|
-
|
|
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 (
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
|
49
|
+
const connections = {};
|
|
50
|
+
const stats = {
|
|
68
51
|
totalRequests: 0,
|
|
69
52
|
successfulRequests: 0,
|
|
70
|
-
|
|
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(
|
|
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;
|
|
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();
|
|
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
|
-
|
|
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
|
-
|
|
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));
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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;
|