@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.
package/src/server.ts ADDED
@@ -0,0 +1,443 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { ServerConfig, ToolDefinition } from './types';
3
+ import { BloggerService } from './bloggerService';
4
+ import { z } from 'zod';
5
+
6
+ /**
7
+ * Creates the tool definitions for the Blogger MCP server
8
+ * @param bloggerService Blogger service to interact with the API
9
+ * @returns Array of tool definitions
10
+ */
11
+ export function createToolDefinitions(bloggerService: BloggerService): ToolDefinition[] {
12
+ return [
13
+ {
14
+ name: 'list_blogs',
15
+ description: 'Lists all accessible blogs',
16
+ args: z.object({}),
17
+ handler: async (_args, _extra) => {
18
+ try {
19
+ const blogs = await bloggerService.listBlogs();
20
+ return {
21
+ content: [
22
+ {
23
+ type: 'text',
24
+ text: JSON.stringify({ blogs }, null, 2)
25
+ }
26
+ ]
27
+ };
28
+ } catch (error) {
29
+ console.error('Error fetching blogs:', error);
30
+ return {
31
+ content: [
32
+ {
33
+ type: 'text',
34
+ text: `Error fetching blogs: ${error}`
35
+ }
36
+ ],
37
+ isError: true
38
+ };
39
+ }
40
+ }
41
+ },
42
+ {
43
+ name: 'get_blog',
44
+ description: 'Retrieves details of a specific blog',
45
+ args: z.object({
46
+ blogId: z.string().describe('Blog ID')
47
+ }),
48
+ handler: async (args, _extra) => {
49
+ try {
50
+ const blog = await bloggerService.getBlog(args.blogId);
51
+ return {
52
+ content: [
53
+ {
54
+ type: 'text',
55
+ text: JSON.stringify({ blog }, null, 2)
56
+ }
57
+ ]
58
+ };
59
+ } catch (error) {
60
+ console.error(`Error fetching blog ${args.blogId}:`, error);
61
+ return {
62
+ content: [
63
+ {
64
+ type: 'text',
65
+ text: `Error fetching blog: ${error}`
66
+ }
67
+ ],
68
+ isError: true
69
+ };
70
+ }
71
+ }
72
+ },
73
+ {
74
+ name: 'get_blog_by_url',
75
+ description: 'Retrieves a blog by its URL (useful for discovering blog ID)',
76
+ args: z.object({
77
+ url: z.string().describe('Blog URL')
78
+ }),
79
+ handler: async (args, _extra) => {
80
+ try {
81
+ const blog = await bloggerService.getBlogByUrl(args.url);
82
+ return {
83
+ content: [
84
+ {
85
+ type: 'text',
86
+ text: JSON.stringify({ blog }, null, 2)
87
+ }
88
+ ]
89
+ };
90
+ } catch (error) {
91
+ console.error(`Error fetching blog by URL ${args.url}:`, error);
92
+ return {
93
+ content: [
94
+ {
95
+ type: 'text',
96
+ text: `Error fetching blog by URL: ${error}`
97
+ }
98
+ ],
99
+ isError: true
100
+ };
101
+ }
102
+ }
103
+ },
104
+ {
105
+ name: 'create_blog',
106
+ description: 'Creates a new blog (not supported by the Blogger API)',
107
+ args: z.object({
108
+ name: z.string().describe('Blog name'),
109
+ description: z.string().optional().describe('Blog description')
110
+ }),
111
+ handler: async (_args, _extra) => {
112
+ return {
113
+ content: [
114
+ {
115
+ type: 'text',
116
+ text: 'Blog creation is not supported by the Blogger API. Please create a blog via the Blogger web interface.'
117
+ }
118
+ ],
119
+ isError: true
120
+ };
121
+ }
122
+ },
123
+ {
124
+ name: 'list_posts',
125
+ description: 'Lists all posts from a blog',
126
+ args: z.object({
127
+ blogId: z.string().describe('Blog ID'),
128
+ maxResults: z.number().optional().describe('Maximum number of results to return')
129
+ }),
130
+ handler: async (args, _extra) => {
131
+ try {
132
+ const posts = await bloggerService.listPosts(args.blogId, args.maxResults);
133
+ return {
134
+ content: [
135
+ {
136
+ type: 'text',
137
+ text: JSON.stringify({ posts }, null, 2)
138
+ }
139
+ ]
140
+ };
141
+ } catch (error) {
142
+ console.error(`Error fetching posts for blog ${args.blogId}:`, error);
143
+ return {
144
+ content: [
145
+ {
146
+ type: 'text',
147
+ text: `Error fetching posts: ${error}`
148
+ }
149
+ ],
150
+ isError: true
151
+ };
152
+ }
153
+ }
154
+ },
155
+ {
156
+ name: 'search_posts',
157
+ description: 'Searches posts in a blog',
158
+ args: z.object({
159
+ blogId: z.string().describe('Blog ID'),
160
+ query: z.string().describe('Search term'),
161
+ maxResults: z.number().optional().describe('Maximum number of results to return')
162
+ }),
163
+ handler: async (args, _extra) => {
164
+ try {
165
+ const posts = await bloggerService.searchPosts(args.blogId, args.query, args.maxResults);
166
+ return {
167
+ content: [
168
+ {
169
+ type: 'text',
170
+ text: JSON.stringify({ posts }, null, 2)
171
+ }
172
+ ]
173
+ };
174
+ } catch (error) {
175
+ console.error(`Error searching posts in blog ${args.blogId}:`, error);
176
+ return {
177
+ content: [
178
+ {
179
+ type: 'text',
180
+ text: `Error searching posts: ${error}`
181
+ }
182
+ ],
183
+ isError: true
184
+ };
185
+ }
186
+ }
187
+ },
188
+ {
189
+ name: 'get_post',
190
+ description: 'Retrieves details of a specific post',
191
+ args: z.object({
192
+ blogId: z.string().describe('Blog ID'),
193
+ postId: z.string().describe('Post ID')
194
+ }),
195
+ handler: async (args, _extra) => {
196
+ try {
197
+ const post = await bloggerService.getPost(args.blogId, args.postId);
198
+ return {
199
+ content: [
200
+ {
201
+ type: 'text',
202
+ text: JSON.stringify({ post }, null, 2)
203
+ }
204
+ ]
205
+ };
206
+ } catch (error) {
207
+ console.error(`Error fetching post ${args.postId}:`, error);
208
+ return {
209
+ content: [
210
+ {
211
+ type: 'text',
212
+ text: `Error fetching post: ${error}`
213
+ }
214
+ ],
215
+ isError: true
216
+ };
217
+ }
218
+ }
219
+ },
220
+ {
221
+ name: 'create_post',
222
+ description: 'Creates a new post in a blog',
223
+ args: z.object({
224
+ blogId: z.string().describe('Blog ID'),
225
+ title: z.string().describe('Post title'),
226
+ content: z.string().describe('Post content'),
227
+ labels: z.array(z.string()).optional().describe('Labels to associate with the post')
228
+ }),
229
+ handler: async (args, _extra) => {
230
+ try {
231
+ const post = await bloggerService.createPost(args.blogId, {
232
+ title: args.title,
233
+ content: args.content,
234
+ labels: args.labels
235
+ });
236
+ return {
237
+ content: [
238
+ {
239
+ type: 'text',
240
+ text: JSON.stringify({ post }, null, 2)
241
+ }
242
+ ]
243
+ };
244
+ } catch (error) {
245
+ console.error(`Error creating post in blog ${args.blogId}:`, error);
246
+ return {
247
+ content: [
248
+ {
249
+ type: 'text',
250
+ text: `Error creating post: ${error}`
251
+ }
252
+ ],
253
+ isError: true
254
+ };
255
+ }
256
+ }
257
+ },
258
+ {
259
+ name: 'update_post',
260
+ description: 'Updates an existing post',
261
+ args: z.object({
262
+ blogId: z.string().describe('Blog ID'),
263
+ postId: z.string().describe('Post ID'),
264
+ title: z.string().optional().describe('New post title'),
265
+ content: z.string().optional().describe('New post content'),
266
+ labels: z.array(z.string()).optional().describe('New labels to associate with the post')
267
+ }),
268
+ handler: async (args, _extra) => {
269
+ try {
270
+ const post = await bloggerService.updatePost(args.blogId, args.postId, {
271
+ title: args.title,
272
+ content: args.content,
273
+ labels: args.labels
274
+ });
275
+ return {
276
+ content: [
277
+ {
278
+ type: 'text',
279
+ text: JSON.stringify({ post }, null, 2)
280
+ }
281
+ ]
282
+ };
283
+ } catch (error) {
284
+ console.error(`Error updating post ${args.postId}:`, error);
285
+ return {
286
+ content: [
287
+ {
288
+ type: 'text',
289
+ text: `Error updating post: ${error}`
290
+ }
291
+ ],
292
+ isError: true
293
+ };
294
+ }
295
+ }
296
+ },
297
+ {
298
+ name: 'delete_post',
299
+ description: 'Deletes a post',
300
+ args: z.object({
301
+ blogId: z.string().describe('Blog ID'),
302
+ postId: z.string().describe('Post ID')
303
+ }),
304
+ handler: async (args, _extra) => {
305
+ try {
306
+ await bloggerService.deletePost(args.blogId, args.postId);
307
+ return {
308
+ content: [
309
+ {
310
+ type: 'text',
311
+ text: JSON.stringify({ success: true }, null, 2)
312
+ }
313
+ ]
314
+ };
315
+ } catch (error) {
316
+ console.error(`Error deleting post ${args.postId}:`, error);
317
+ return {
318
+ content: [
319
+ {
320
+ type: 'text',
321
+ text: `Error deleting post: ${error}`
322
+ }
323
+ ],
324
+ isError: true
325
+ };
326
+ }
327
+ }
328
+ },
329
+ {
330
+ name: 'list_labels',
331
+ description: 'Lists all labels from a blog',
332
+ args: z.object({
333
+ blogId: z.string().describe('Blog ID')
334
+ }),
335
+ handler: async (args, _extra) => {
336
+ try {
337
+ const labels = await bloggerService.listLabels(args.blogId);
338
+ return {
339
+ content: [
340
+ {
341
+ type: 'text',
342
+ text: JSON.stringify({ labels }, null, 2)
343
+ }
344
+ ]
345
+ };
346
+ } catch (error) {
347
+ console.error(`Error fetching labels for blog ${args.blogId}:`, error);
348
+ return {
349
+ content: [
350
+ {
351
+ type: 'text',
352
+ text: `Error fetching labels: ${error}`
353
+ }
354
+ ],
355
+ isError: true
356
+ };
357
+ }
358
+ }
359
+ },
360
+ {
361
+ name: 'get_label',
362
+ description: 'Retrieves details of a specific label',
363
+ args: z.object({
364
+ blogId: z.string().describe('Blog ID'),
365
+ labelName: z.string().describe('Label name')
366
+ }),
367
+ handler: async (args, _extra) => {
368
+ try {
369
+ const label = await bloggerService.getLabel(args.blogId, args.labelName);
370
+ return {
371
+ content: [
372
+ {
373
+ type: 'text',
374
+ text: JSON.stringify({ label }, null, 2)
375
+ }
376
+ ]
377
+ };
378
+ } catch (error) {
379
+ console.error(`Error fetching label ${args.labelName}:`, error);
380
+ return {
381
+ content: [
382
+ {
383
+ type: 'text',
384
+ text: `Error fetching label: ${error}`
385
+ }
386
+ ],
387
+ isError: true
388
+ };
389
+ }
390
+ }
391
+ }
392
+ ];
393
+ }
394
+
395
+ /**
396
+ * Initializes the MCP server with all Blogger tools
397
+ * @param bloggerService Blogger service to interact with the API
398
+ * @param config Server configuration
399
+ * @returns MCP server instance
400
+ */
401
+ export function initMCPServer(bloggerService: BloggerService, config: ServerConfig): McpServer {
402
+ // Create a new MCP server instance with server information
403
+ const server = new McpServer({
404
+ name: "Blogger MCP Server",
405
+ version: "1.0.4",
406
+ vendor: "mcproadev"
407
+ });
408
+
409
+ // Get all tool definitions
410
+ const toolDefinitions = createToolDefinitions(bloggerService);
411
+
412
+ // Register each tool with the MCP server
413
+ for (const tool of toolDefinitions) {
414
+ // We can't directly pass the schema object if it's already a Zod object in our definition,
415
+ // The MCP SDK expects the shape, not the Zod object itself for the 'args' parameter in server.tool()
416
+ // However, looking at the previous code:
417
+ // server.tool('name', 'desc', { param: z.string() }, handler)
418
+ // The previous code passed an object with Zod schemas as values.
419
+ // Our ToolDefinition.args is a z.ZodType<any>, which is likely a z.object({...}).
420
+ // We need to extract the shape from the z.object to pass it to server.tool if we want to match the signature.
421
+
422
+ // Actually, looking at the SDK, server.tool takes:
423
+ // name: string, description: string, args: ToolArgs, handler: ToolCallback
424
+ // where ToolArgs is Record<string, ZodType<any>>
425
+
426
+ // So my ToolDefinition.args should probably be Record<string, ZodType<any>> instead of z.ZodType<any>
427
+ // to make it easier to spread.
428
+
429
+ // Let's adjust the implementation in the loop.
430
+ // Since I defined args as z.ZodType<any> (which is z.object({...})), I can cast it or access .shape if it's a ZodObject.
431
+
432
+ if (tool.args instanceof z.ZodObject) {
433
+ server.tool(tool.name, tool.description, tool.args.shape, tool.handler);
434
+ } else {
435
+ // Fallback for empty objects or other schemas if we had them (list_blogs has empty object)
436
+ // If it's not a ZodObject, we might have issues if the SDK expects a shape map.
437
+ // list_blogs used {} which is compatible with Record<string, ZodType>
438
+ server.tool(tool.name, tool.description, {}, tool.handler);
439
+ }
440
+ }
441
+
442
+ return server;
443
+ }
package/src/types.ts ADDED
@@ -0,0 +1,113 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * Types used in the MCP server for Blogger
5
+ */
6
+
7
+ // Tool definition type
8
+ export interface ToolDefinition {
9
+ name: string;
10
+ description: string;
11
+ args: z.ZodType<any>;
12
+ handler: (args: any, extra?: any) => Promise<any>;
13
+ }
14
+
15
+ // Blog type
16
+ export interface BloggerBlog {
17
+ id: string;
18
+ name: string;
19
+ description?: string;
20
+ url: string;
21
+ status?: string;
22
+ posts?: BloggerPost[];
23
+ labels?: BloggerLabel[];
24
+ }
25
+
26
+ // Post type
27
+ export interface BloggerPost {
28
+ id: string;
29
+ blogId: string;
30
+ title: string;
31
+ content: string;
32
+ url?: string;
33
+ published?: string;
34
+ updated?: string;
35
+ author?: {
36
+ id: string;
37
+ displayName: string;
38
+ url: string;
39
+ image?: {
40
+ url: string;
41
+ };
42
+ };
43
+ labels?: string[];
44
+ }
45
+
46
+ // Label type
47
+ export interface BloggerLabel {
48
+ id?: string;
49
+ name: string;
50
+ }
51
+
52
+ // Search parameters type
53
+ export interface SearchParams {
54
+ query: string;
55
+ maxResults?: number;
56
+ }
57
+
58
+ // Pagination parameters type
59
+ export interface PaginationParams {
60
+ pageToken?: string;
61
+ maxResults?: number;
62
+ }
63
+
64
+ // Server operating modes type
65
+ export type ServerMode =
66
+ | { type: 'stdio' }
67
+ | { type: 'http', host: string, port: number };
68
+
69
+ // OAuth2 configuration type
70
+ export interface OAuth2Config {
71
+ clientId?: string;
72
+ clientSecret?: string;
73
+ refreshToken?: string;
74
+ }
75
+
76
+ // Server configuration type
77
+ export interface ServerConfig {
78
+ mode: ServerMode;
79
+ blogger: {
80
+ apiKey?: string;
81
+ maxResults: number;
82
+ timeout: number;
83
+ };
84
+ oauth2: OAuth2Config;
85
+ logging: {
86
+ level: string;
87
+ };
88
+ }
89
+
90
+ // UI types
91
+ export interface ServerStatus {
92
+ running: boolean;
93
+ mode: string;
94
+ startTime?: Date;
95
+ connections: number;
96
+ tools: string[];
97
+ }
98
+
99
+ export interface ClientConnection {
100
+ id: string;
101
+ ip?: string;
102
+ connectedAt: Date;
103
+ lastActivity: Date;
104
+ requestCount: number;
105
+ }
106
+
107
+ export interface ServerStats {
108
+ totalRequests: number;
109
+ successfulRequests: number;
110
+ failedRequests: number;
111
+ averageResponseTime: number;
112
+ toolUsage: Record<string, number>;
113
+ }
@@ -0,0 +1,128 @@
1
+ import express from 'express';
2
+ import path from 'path';
3
+ import { Server as HttpServer } from 'http';
4
+ import { Server as SocketIOServer } from 'socket.io';
5
+ import { ServerStatus, ClientConnection, ServerStats } from './types';
6
+
7
+ // UI manager interface
8
+ export interface UIManager {
9
+ start(port: number): Promise<void>;
10
+ stop(): Promise<void>;
11
+ updateStatus(status: ServerStatus): void;
12
+ updateConnections(connections: ClientConnection[]): void;
13
+ updateStats(stats: ServerStats): void;
14
+ }
15
+
16
+ // UI manager implementation
17
+ export class WebUIManager implements UIManager {
18
+ private app: express.Application;
19
+ private server: HttpServer | null = null;
20
+ private io: SocketIOServer | null = null;
21
+ private status: ServerStatus = {
22
+ running: false,
23
+ mode: 'stopped',
24
+ connections: 0,
25
+ tools: []
26
+ };
27
+ private connections: ClientConnection[] = [];
28
+ private stats: ServerStats = {
29
+ totalRequests: 0,
30
+ successfulRequests: 0,
31
+ failedRequests: 0,
32
+ averageResponseTime: 0,
33
+ toolUsage: {}
34
+ };
35
+
36
+ constructor() {
37
+ this.app = express();
38
+
39
+ // Express configuration
40
+ this.app.use(express.json());
41
+ this.app.use(express.static(path.join(__dirname, '../public')));
42
+
43
+ // API routes
44
+ this.app.get('/api/status', (req, res) => {
45
+ res.json(this.status);
46
+ });
47
+
48
+ this.app.get('/api/connections', (req, res) => {
49
+ res.json(this.connections);
50
+ });
51
+
52
+ this.app.get('/api/stats', (req, res) => {
53
+ res.json(this.stats);
54
+ });
55
+
56
+ // Main route for the UI - wildcard route fix
57
+ this.app.get('/', (req, res) => {
58
+ res.sendFile(path.join(__dirname, '../public/index.html'));
59
+ });
60
+ }
61
+
62
+ async start(port: number): Promise<void> {
63
+ return new Promise((resolve) => {
64
+ this.server = new HttpServer(this.app);
65
+ this.io = new SocketIOServer(this.server);
66
+
67
+ // Socket.IO configuration
68
+ this.io.on('connection', (socket) => {
69
+ console.log('New UI connection:', socket.id);
70
+
71
+ // Send initial data
72
+ socket.emit('status', this.status);
73
+ socket.emit('connections', this.connections);
74
+ socket.emit('stats', this.stats);
75
+
76
+ // Handle user actions
77
+ socket.on('restart-server', () => {
78
+ console.log('Server restart request received');
79
+ // Restart logic to be implemented
80
+ });
81
+ });
82
+
83
+ this.server.listen(port, () => {
84
+ console.log(`Web UI started on port ${port}`);
85
+ resolve();
86
+ });
87
+ });
88
+ }
89
+
90
+ async stop(): Promise<void> {
91
+ return new Promise((resolve, reject) => {
92
+ if (this.server) {
93
+ this.server.close((err) => {
94
+ if (err) {
95
+ reject(err);
96
+ } else {
97
+ this.server = null;
98
+ this.io = null;
99
+ resolve();
100
+ }
101
+ });
102
+ } else {
103
+ resolve();
104
+ }
105
+ });
106
+ }
107
+
108
+ updateStatus(status: ServerStatus): void {
109
+ this.status = status;
110
+ if (this.io) {
111
+ this.io.emit('status', status);
112
+ }
113
+ }
114
+
115
+ updateConnections(connections: ClientConnection[]): void {
116
+ this.connections = connections;
117
+ if (this.io) {
118
+ this.io.emit('connections', connections);
119
+ }
120
+ }
121
+
122
+ updateStats(stats: ServerStats): void {
123
+ this.stats = stats;
124
+ if (this.io) {
125
+ this.io.emit('stats', stats);
126
+ }
127
+ }
128
+ }