@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/.github/workflows/publish.yml +34 -0
- package/AGENTS.md +155 -0
- package/Dockerfile +64 -0
- package/README.md +169 -0
- package/RELEASE.md +125 -0
- package/dist/bloggerService.d.ts +121 -0
- package/dist/bloggerService.js +323 -0
- package/dist/config.d.ts +20 -0
- package/dist/config.js +32 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +304 -0
- package/dist/mcp-sdk-mock.d.ts +57 -0
- package/dist/mcp-sdk-mock.js +227 -0
- package/dist/server.d.ts +16 -0
- package/dist/server.js +448 -0
- package/dist/types.d.ts +94 -0
- package/dist/types.js +2 -0
- package/dist/ui-manager.d.ts +22 -0
- package/dist/ui-manager.js +110 -0
- package/jest.config.js +7 -0
- package/package.json +43 -0
- package/public/index.html +201 -0
- package/public/main.js +271 -0
- package/public/styles.css +155 -0
- package/src/bloggerService.test.ts +398 -0
- package/src/bloggerService.ts +351 -0
- package/src/config.test.ts +121 -0
- package/src/config.ts +33 -0
- package/src/index.ts +349 -0
- package/src/server.ts +443 -0
- package/src/types.ts +113 -0
- package/src/ui-manager.ts +128 -0
- package/start-dev.sh +64 -0
- package/start-prod.sh +53 -0
- package/tsconfig.json +15 -0
- package/vercel.json +24 -0
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
|
+
}
|