@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/src/server.ts CHANGED
@@ -1,43 +1,37 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- import { ServerConfig, ToolDefinition } from './types';
2
+ import { ServerConfig, ToolDefinition, ToolResult } from './types';
3
3
  import { BloggerService } from './bloggerService';
4
4
  import { z } from 'zod';
5
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
- */
6
+ function createToolHandler(
7
+ toolName: string,
8
+ fn: () => Promise<unknown>
9
+ ): () => Promise<ToolResult> {
10
+ return async () => {
11
+ try {
12
+ const result = await fn();
13
+ return {
14
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
15
+ };
16
+ } catch (error) {
17
+ console.error(`Error in ${toolName}:`, error);
18
+ return {
19
+ content: [{ type: 'text', text: `Error in ${toolName}: ${error}` }],
20
+ isError: true
21
+ };
22
+ }
23
+ };
24
+ }
25
+
11
26
  export function createToolDefinitions(bloggerService: BloggerService): ToolDefinition[] {
12
27
  return [
13
28
  {
14
29
  name: 'list_blogs',
15
30
  description: 'Lists all accessible blogs',
16
31
  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
- }
32
+ handler: createToolHandler('list_blogs', () => {
33
+ return bloggerService.listBlogs().then(blogs => ({ blogs }));
34
+ })
41
35
  },
42
36
  {
43
37
  name: 'get_blog',
@@ -45,30 +39,9 @@ export function createToolDefinitions(bloggerService: BloggerService): ToolDefin
45
39
  args: z.object({
46
40
  blogId: z.string().describe('Blog ID')
47
41
  }),
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
- }
42
+ handler: (args) => createToolHandler('get_blog', () => {
43
+ return bloggerService.getBlog(args.blogId).then(blog => ({ blog }));
44
+ })()
72
45
  },
73
46
  {
74
47
  name: 'get_blog_by_url',
@@ -76,49 +49,9 @@ export function createToolDefinitions(bloggerService: BloggerService): ToolDefin
76
49
  args: z.object({
77
50
  url: z.string().describe('Blog URL')
78
51
  }),
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
- }
52
+ handler: (args) => createToolHandler('get_blog_by_url', () => {
53
+ return bloggerService.getBlogByUrl(args.url).then(blog => ({ blog }));
54
+ })()
122
55
  },
123
56
  {
124
57
  name: 'list_posts',
@@ -127,30 +60,9 @@ export function createToolDefinitions(bloggerService: BloggerService): ToolDefin
127
60
  blogId: z.string().describe('Blog ID'),
128
61
  maxResults: z.number().optional().describe('Maximum number of results to return')
129
62
  }),
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
- }
63
+ handler: (args) => createToolHandler('list_posts', () => {
64
+ return bloggerService.listPosts(args.blogId, args.maxResults).then(posts => ({ posts }));
65
+ })()
154
66
  },
155
67
  {
156
68
  name: 'search_posts',
@@ -160,30 +72,9 @@ export function createToolDefinitions(bloggerService: BloggerService): ToolDefin
160
72
  query: z.string().describe('Search term'),
161
73
  maxResults: z.number().optional().describe('Maximum number of results to return')
162
74
  }),
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
- }
75
+ handler: (args) => createToolHandler('search_posts', () => {
76
+ return bloggerService.searchPosts(args.blogId, args.query, args.maxResults).then(posts => ({ posts }));
77
+ })()
187
78
  },
188
79
  {
189
80
  name: 'get_post',
@@ -192,30 +83,9 @@ export function createToolDefinitions(bloggerService: BloggerService): ToolDefin
192
83
  blogId: z.string().describe('Blog ID'),
193
84
  postId: z.string().describe('Post ID')
194
85
  }),
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
- }
86
+ handler: (args) => createToolHandler('get_post', () => {
87
+ return bloggerService.getPost(args.blogId, args.postId).then(post => ({ post }));
88
+ })()
219
89
  },
220
90
  {
221
91
  name: 'create_post',
@@ -226,34 +96,13 @@ export function createToolDefinitions(bloggerService: BloggerService): ToolDefin
226
96
  content: z.string().describe('Post content'),
227
97
  labels: z.array(z.string()).optional().describe('Labels to associate with the post')
228
98
  }),
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
- }
99
+ handler: (args) => createToolHandler('create_post', () => {
100
+ return bloggerService.createPost(args.blogId, {
101
+ title: args.title,
102
+ content: args.content,
103
+ labels: args.labels
104
+ }).then(post => ({ post }));
105
+ })()
257
106
  },
258
107
  {
259
108
  name: 'update_post',
@@ -265,34 +114,13 @@ export function createToolDefinitions(bloggerService: BloggerService): ToolDefin
265
114
  content: z.string().optional().describe('New post content'),
266
115
  labels: z.array(z.string()).optional().describe('New labels to associate with the post')
267
116
  }),
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
- }
117
+ handler: (args) => createToolHandler('update_post', () => {
118
+ return bloggerService.updatePost(args.blogId, args.postId, {
119
+ title: args.title,
120
+ content: args.content,
121
+ labels: args.labels
122
+ }).then(post => ({ post }));
123
+ })()
296
124
  },
297
125
  {
298
126
  name: 'delete_post',
@@ -301,30 +129,9 @@ export function createToolDefinitions(bloggerService: BloggerService): ToolDefin
301
129
  blogId: z.string().describe('Blog ID'),
302
130
  postId: z.string().describe('Post ID')
303
131
  }),
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
- }
132
+ handler: (args) => createToolHandler('delete_post', () => {
133
+ return bloggerService.deletePost(args.blogId, args.postId).then(() => ({ success: true }));
134
+ })()
328
135
  },
329
136
  {
330
137
  name: 'list_labels',
@@ -332,30 +139,9 @@ export function createToolDefinitions(bloggerService: BloggerService): ToolDefin
332
139
  args: z.object({
333
140
  blogId: z.string().describe('Blog ID')
334
141
  }),
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
- }
142
+ handler: (args) => createToolHandler('list_labels', () => {
143
+ return bloggerService.listLabels(args.blogId).then(labels => ({ labels: labels.items }));
144
+ })()
359
145
  },
360
146
  {
361
147
  name: 'get_label',
@@ -364,79 +150,24 @@ export function createToolDefinitions(bloggerService: BloggerService): ToolDefin
364
150
  blogId: z.string().describe('Blog ID'),
365
151
  labelName: z.string().describe('Label name')
366
152
  }),
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
- }
153
+ handler: (args) => createToolHandler('get_label', () => {
154
+ return bloggerService.getLabel(args.blogId, args.labelName).then(label => ({ label }));
155
+ })()
391
156
  }
392
157
  ];
393
158
  }
394
159
 
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
160
  export function initMCPServer(bloggerService: BloggerService, config: ServerConfig): McpServer {
402
- // Create a new MCP server instance with server information
403
161
  const server = new McpServer({
404
162
  name: "Blogger MCP Server",
405
163
  version: "1.0.4",
406
164
  vendor: "mcproadev"
407
165
  });
408
166
 
409
- // Get all tool definitions
410
167
  const toolDefinitions = createToolDefinitions(bloggerService);
411
168
 
412
- // Register each tool with the MCP server
413
169
  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
- }
170
+ server.tool(tool.name, tool.description, tool.args.shape, tool.handler as never);
440
171
  }
441
172
 
442
173
  return server;
package/src/types.ts CHANGED
@@ -1,79 +1,32 @@
1
1
  import { z } from 'zod';
2
+ import { blogger_v3 } from 'googleapis';
2
3
 
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>;
4
+ export interface ToolResult {
5
+ content: Array<{ type: string; text: string }>;
6
+ isError?: boolean;
13
7
  }
14
8
 
15
- // Blog type
16
- export interface BloggerBlog {
17
- id: string;
9
+ export interface ToolDefinition<T extends z.ZodRawShape = z.ZodRawShape> {
18
10
  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;
11
+ description: string;
12
+ args: z.ZodObject<T>;
13
+ handler: (args: z.infer<z.ZodObject<T>>) => Promise<ToolResult>;
56
14
  }
57
15
 
58
- // Pagination parameters type
59
- export interface PaginationParams {
60
- pageToken?: string;
61
- maxResults?: number;
62
- }
16
+ export type BloggerBlog = blogger_v3.Schema$Blog;
17
+ export type BloggerPost = blogger_v3.Schema$Post;
18
+ export type BloggerLabel = { name: string };
63
19
 
64
- // Server operating modes type
65
20
  export type ServerMode =
66
21
  | { type: 'stdio' }
67
- | { type: 'http', host: string, port: number };
22
+ | { type: 'http'; host: string; port: number };
68
23
 
69
- // OAuth2 configuration type
70
24
  export interface OAuth2Config {
71
25
  clientId?: string;
72
26
  clientSecret?: string;
73
27
  refreshToken?: string;
74
28
  }
75
29
 
76
- // Server configuration type
77
30
  export interface ServerConfig {
78
31
  mode: ServerMode;
79
32
  blogger: {
@@ -87,7 +40,6 @@ export interface ServerConfig {
87
40
  };
88
41
  }
89
42
 
90
- // UI types
91
43
  export interface ServerStatus {
92
44
  running: boolean;
93
45
  mode: string;
package/src/ui-manager.ts CHANGED
@@ -4,7 +4,6 @@ import { Server as HttpServer } from 'http';
4
4
  import { Server as SocketIOServer } from 'socket.io';
5
5
  import { ServerStatus, ClientConnection, ServerStats } from './types';
6
6
 
7
- // UI manager interface
8
7
  export interface UIManager {
9
8
  start(port: number): Promise<void>;
10
9
  stop(): Promise<void>;
@@ -13,7 +12,6 @@ export interface UIManager {
13
12
  updateStats(stats: ServerStats): void;
14
13
  }
15
14
 
16
- // UI manager implementation
17
15
  export class WebUIManager implements UIManager {
18
16
  private app: express.Application;
19
17
  private server: HttpServer | null = null;
@@ -35,51 +33,44 @@ export class WebUIManager implements UIManager {
35
33
 
36
34
  constructor() {
37
35
  this.app = express();
38
-
39
- // Express configuration
36
+
40
37
  this.app.use(express.json());
41
38
  this.app.use(express.static(path.join(__dirname, '../public')));
42
-
43
- // API routes
44
- this.app.get('/api/status', (req, res) => {
39
+
40
+ this.app.get('/api/status', (_req, res) => {
45
41
  res.json(this.status);
46
42
  });
47
-
48
- this.app.get('/api/connections', (req, res) => {
43
+
44
+ this.app.get('/api/connections', (_req, res) => {
49
45
  res.json(this.connections);
50
46
  });
51
-
52
- this.app.get('/api/stats', (req, res) => {
47
+
48
+ this.app.get('/api/stats', (_req, res) => {
53
49
  res.json(this.stats);
54
50
  });
55
-
56
- // Main route for the UI - wildcard route fix
57
- this.app.get('/', (req, res) => {
51
+
52
+ this.app.get('/', (_req, res) => {
58
53
  res.sendFile(path.join(__dirname, '../public/index.html'));
59
54
  });
60
55
  }
61
56
 
62
57
  async start(port: number): Promise<void> {
63
- return new Promise((resolve) => {
58
+ return new Promise((resolve, reject) => {
64
59
  this.server = new HttpServer(this.app);
65
60
  this.io = new SocketIOServer(this.server);
66
-
67
- // Socket.IO configuration
61
+
62
+ this.server.on('error', (err) => {
63
+ reject(err);
64
+ });
65
+
68
66
  this.io.on('connection', (socket) => {
69
67
  console.log('New UI connection:', socket.id);
70
-
71
- // Send initial data
68
+
72
69
  socket.emit('status', this.status);
73
70
  socket.emit('connections', this.connections);
74
71
  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
72
  });
82
-
73
+
83
74
  this.server.listen(port, () => {
84
75
  console.log(`Web UI started on port ${port}`);
85
76
  resolve();