@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
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import { google, blogger_v3 } from 'googleapis';
|
|
2
|
+
import { BloggerBlog, BloggerPost, BloggerLabel } from './types';
|
|
3
|
+
import { config } from './config';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Custom types to compensate for Blogger API limitations
|
|
7
|
+
*/
|
|
8
|
+
interface BloggerLabelList {
|
|
9
|
+
kind?: string;
|
|
10
|
+
items?: BloggerLabel[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Google Blogger API interaction service
|
|
15
|
+
*
|
|
16
|
+
* Supports two authentication modes:
|
|
17
|
+
* - OAuth2 (GOOGLE_CLIENT_ID + GOOGLE_CLIENT_SECRET + GOOGLE_REFRESH_TOKEN):
|
|
18
|
+
* full access (read + write). Required for listBlogs, createPost, updatePost, deletePost.
|
|
19
|
+
* - API Key (BLOGGER_API_KEY): read-only access to public blogs.
|
|
20
|
+
* Works for getBlog, listPosts, getPost, searchPosts, listLabels, getLabel.
|
|
21
|
+
*
|
|
22
|
+
* If both are configured, OAuth2 is used (it covers all operations).
|
|
23
|
+
*/
|
|
24
|
+
export class BloggerService {
|
|
25
|
+
private blogger: blogger_v3.Blogger;
|
|
26
|
+
private readonly isOAuth2: boolean;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Initializes the Blogger service with OAuth2 or API key
|
|
30
|
+
*/
|
|
31
|
+
constructor() {
|
|
32
|
+
const { oauth2 } = config;
|
|
33
|
+
const hasOAuth2 = !!(oauth2.clientId && oauth2.clientSecret && oauth2.refreshToken);
|
|
34
|
+
|
|
35
|
+
if (hasOAuth2) {
|
|
36
|
+
const oauth2Client = new google.auth.OAuth2(
|
|
37
|
+
oauth2.clientId,
|
|
38
|
+
oauth2.clientSecret
|
|
39
|
+
);
|
|
40
|
+
oauth2Client.setCredentials({ refresh_token: oauth2.refreshToken });
|
|
41
|
+
|
|
42
|
+
this.blogger = google.blogger({
|
|
43
|
+
version: 'v3',
|
|
44
|
+
auth: oauth2Client,
|
|
45
|
+
timeout: config.blogger.timeout
|
|
46
|
+
});
|
|
47
|
+
this.isOAuth2 = true;
|
|
48
|
+
console.log('BloggerService initialized with OAuth2 (full access)');
|
|
49
|
+
} else if (config.blogger.apiKey) {
|
|
50
|
+
this.blogger = google.blogger({
|
|
51
|
+
version: 'v3',
|
|
52
|
+
auth: config.blogger.apiKey,
|
|
53
|
+
timeout: config.blogger.timeout
|
|
54
|
+
});
|
|
55
|
+
this.isOAuth2 = false;
|
|
56
|
+
console.log('BloggerService initialized with API Key (read-only)');
|
|
57
|
+
} else {
|
|
58
|
+
throw new Error(
|
|
59
|
+
'No authentication configured. ' +
|
|
60
|
+
'Set BLOGGER_API_KEY (read-only) or ' +
|
|
61
|
+
'GOOGLE_CLIENT_ID + GOOGLE_CLIENT_SECRET + GOOGLE_REFRESH_TOKEN (full access).'
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Checks that OAuth2 authentication is available.
|
|
68
|
+
* Throws an explicit error if the operation requires OAuth2 and we are in API key mode.
|
|
69
|
+
*/
|
|
70
|
+
private requireOAuth2(operation: string): void {
|
|
71
|
+
if (!this.isOAuth2) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`Operation "${operation}" requires OAuth2 authentication. ` +
|
|
74
|
+
'API Key mode only allows reading public blogs. ' +
|
|
75
|
+
'Configure GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET and GOOGLE_REFRESH_TOKEN.'
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Lists all blogs for the authenticated user.
|
|
82
|
+
* Requires OAuth2 (blogs.listByUser with userId: 'self').
|
|
83
|
+
* @returns Blog list
|
|
84
|
+
*/
|
|
85
|
+
async listBlogs(): Promise<blogger_v3.Schema$BlogList> {
|
|
86
|
+
this.requireOAuth2('list_blogs');
|
|
87
|
+
try {
|
|
88
|
+
const response = await this.blogger.blogs.listByUser({
|
|
89
|
+
userId: 'self'
|
|
90
|
+
});
|
|
91
|
+
return response.data;
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.error('Error fetching blogs:', error);
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Retrieves details of a specific blog
|
|
100
|
+
* @param blogId ID of the blog to retrieve
|
|
101
|
+
* @returns Blog details
|
|
102
|
+
*/
|
|
103
|
+
async getBlog(blogId: string): Promise<blogger_v3.Schema$Blog> {
|
|
104
|
+
try {
|
|
105
|
+
const response = await this.blogger.blogs.get({
|
|
106
|
+
blogId
|
|
107
|
+
});
|
|
108
|
+
return response.data;
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error(`Error fetching blog ${blogId}:`, error);
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Retrieves a blog by its URL
|
|
117
|
+
* @param url Blog URL
|
|
118
|
+
* @returns Blog details
|
|
119
|
+
*/
|
|
120
|
+
async getBlogByUrl(url: string): Promise<blogger_v3.Schema$Blog> {
|
|
121
|
+
try {
|
|
122
|
+
const response = await this.blogger.blogs.getByUrl({
|
|
123
|
+
url
|
|
124
|
+
});
|
|
125
|
+
return response.data;
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.error(`Error fetching blog by URL ${url}:`, error);
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Simulates blog creation.
|
|
134
|
+
* Note: The Blogger API does not actually allow creating a blog via API.
|
|
135
|
+
* This method simulates the functionality and returns an explanatory error message.
|
|
136
|
+
*
|
|
137
|
+
* @param blogData Blog data to create
|
|
138
|
+
* @returns Explanatory error message
|
|
139
|
+
*/
|
|
140
|
+
async createBlog(blogData: Partial<BloggerBlog>): Promise<any> {
|
|
141
|
+
// Simulate a delay to make the response more realistic
|
|
142
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
143
|
+
|
|
144
|
+
// Return an explanatory error message
|
|
145
|
+
return {
|
|
146
|
+
error: true,
|
|
147
|
+
message: "The Google Blogger API does not allow creating a new blog via API. Please create a blog manually on blogger.com.",
|
|
148
|
+
details: "This limitation is documented by Google. Blogs must be created via the Blogger web interface.",
|
|
149
|
+
suggestedAction: "Create a blog at https://www.blogger.com, then use its ID with this MCP server."
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Lists posts from a blog
|
|
155
|
+
* @param blogId Blog ID
|
|
156
|
+
* @param maxResults Maximum number of results to return
|
|
157
|
+
* @returns Post list
|
|
158
|
+
*/
|
|
159
|
+
async listPosts(blogId: string, maxResults?: number): Promise<blogger_v3.Schema$PostList> {
|
|
160
|
+
try {
|
|
161
|
+
const response = await this.blogger.posts.list({
|
|
162
|
+
blogId,
|
|
163
|
+
maxResults: maxResults || config.blogger.maxResults
|
|
164
|
+
});
|
|
165
|
+
return response.data;
|
|
166
|
+
} catch (error) {
|
|
167
|
+
console.error(`Error fetching posts for blog ${blogId}:`, error);
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Searches posts in a blog using the native posts.search endpoint of the Blogger API
|
|
174
|
+
* @param blogId Blog ID
|
|
175
|
+
* @param query Search term
|
|
176
|
+
* @param maxResults Maximum number of results to return
|
|
177
|
+
* @returns List of matching posts
|
|
178
|
+
*/
|
|
179
|
+
async searchPosts(blogId: string, query: string, maxResults?: number): Promise<blogger_v3.Schema$PostList> {
|
|
180
|
+
try {
|
|
181
|
+
const response = await this.blogger.posts.search({
|
|
182
|
+
blogId,
|
|
183
|
+
q: query,
|
|
184
|
+
fetchBodies: true
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// The search endpoint does not support maxResults directly,
|
|
188
|
+
// so we truncate client-side if needed
|
|
189
|
+
const items = response.data.items || [];
|
|
190
|
+
const limit = maxResults || config.blogger.maxResults;
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
kind: response.data.kind,
|
|
194
|
+
items: items.slice(0, limit)
|
|
195
|
+
};
|
|
196
|
+
} catch (error) {
|
|
197
|
+
console.error(`Error searching posts in blog ${blogId}:`, error);
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Retrieves a specific post
|
|
204
|
+
* @param blogId Blog ID
|
|
205
|
+
* @param postId Post ID
|
|
206
|
+
* @returns Post details
|
|
207
|
+
*/
|
|
208
|
+
async getPost(blogId: string, postId: string): Promise<blogger_v3.Schema$Post> {
|
|
209
|
+
try {
|
|
210
|
+
const response = await this.blogger.posts.get({
|
|
211
|
+
blogId,
|
|
212
|
+
postId
|
|
213
|
+
});
|
|
214
|
+
return response.data;
|
|
215
|
+
} catch (error) {
|
|
216
|
+
console.error(`Error fetching post ${postId}:`, error);
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Creates a new post in a blog.
|
|
223
|
+
* Requires OAuth2.
|
|
224
|
+
* @param blogId Blog ID
|
|
225
|
+
* @param postData Post data to create
|
|
226
|
+
* @returns Created post
|
|
227
|
+
*/
|
|
228
|
+
async createPost(blogId: string, postData: Partial<BloggerPost>): Promise<blogger_v3.Schema$Post> {
|
|
229
|
+
this.requireOAuth2('create_post');
|
|
230
|
+
try {
|
|
231
|
+
const response = await this.blogger.posts.insert({
|
|
232
|
+
blogId,
|
|
233
|
+
requestBody: postData as blogger_v3.Schema$Post
|
|
234
|
+
});
|
|
235
|
+
return response.data;
|
|
236
|
+
} catch (error) {
|
|
237
|
+
console.error(`Error creating post in blog ${blogId}:`, error);
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Updates an existing post.
|
|
244
|
+
* Requires OAuth2.
|
|
245
|
+
* @param blogId Blog ID
|
|
246
|
+
* @param postId Post ID
|
|
247
|
+
* @param postData Post data to update
|
|
248
|
+
* @returns Updated post
|
|
249
|
+
*/
|
|
250
|
+
async updatePost(blogId: string, postId: string, postData: Partial<BloggerPost>): Promise<blogger_v3.Schema$Post> {
|
|
251
|
+
this.requireOAuth2('update_post');
|
|
252
|
+
try {
|
|
253
|
+
// Convert types to avoid compilation errors
|
|
254
|
+
const requestBody: blogger_v3.Schema$Post = {
|
|
255
|
+
title: postData.title,
|
|
256
|
+
content: postData.content,
|
|
257
|
+
labels: postData.labels
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const response = await this.blogger.posts.update({
|
|
261
|
+
blogId,
|
|
262
|
+
postId,
|
|
263
|
+
requestBody
|
|
264
|
+
});
|
|
265
|
+
return response.data;
|
|
266
|
+
} catch (error) {
|
|
267
|
+
console.error(`Error updating post ${postId}:`, error);
|
|
268
|
+
throw error;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Deletes a post.
|
|
274
|
+
* Requires OAuth2.
|
|
275
|
+
* @param blogId Blog ID
|
|
276
|
+
* @param postId Post ID
|
|
277
|
+
* @returns Deletion status
|
|
278
|
+
*/
|
|
279
|
+
async deletePost(blogId: string, postId: string): Promise<void> {
|
|
280
|
+
this.requireOAuth2('delete_post');
|
|
281
|
+
try {
|
|
282
|
+
await this.blogger.posts.delete({
|
|
283
|
+
blogId,
|
|
284
|
+
postId
|
|
285
|
+
});
|
|
286
|
+
} catch (error) {
|
|
287
|
+
console.error(`Error deleting post ${postId}:`, error);
|
|
288
|
+
throw error;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Lists labels from a blog
|
|
294
|
+
* @param blogId Blog ID
|
|
295
|
+
* @returns Label list
|
|
296
|
+
*/
|
|
297
|
+
async listLabels(blogId: string): Promise<BloggerLabelList> {
|
|
298
|
+
try {
|
|
299
|
+
// The Blogger API does not provide a direct endpoint to list labels
|
|
300
|
+
// We fetch all posts and extract unique labels
|
|
301
|
+
const response = await this.blogger.posts.list({
|
|
302
|
+
blogId,
|
|
303
|
+
maxResults: 50 // Fetch enough posts to extract labels
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
const posts = response.data.items || [];
|
|
307
|
+
const labelSet = new Set<string>();
|
|
308
|
+
|
|
309
|
+
// Extract all unique labels from posts
|
|
310
|
+
posts.forEach(post => {
|
|
311
|
+
const postLabels = post.labels || [];
|
|
312
|
+
postLabels.forEach(label => labelSet.add(label));
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// Convert to expected format
|
|
316
|
+
const labels = Array.from(labelSet).map(name => ({ name }));
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
kind: 'blogger#labelList',
|
|
320
|
+
items: labels
|
|
321
|
+
};
|
|
322
|
+
} catch (error) {
|
|
323
|
+
console.error(`Error fetching labels for blog ${blogId}:`, error);
|
|
324
|
+
throw error;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Retrieves a specific label
|
|
330
|
+
* @param blogId Blog ID
|
|
331
|
+
* @param labelName Label name
|
|
332
|
+
* @returns Label details
|
|
333
|
+
*/
|
|
334
|
+
async getLabel(blogId: string, labelName: string): Promise<BloggerLabel> {
|
|
335
|
+
try {
|
|
336
|
+
// The Blogger API does not provide a direct endpoint to retrieve a label
|
|
337
|
+
// We check if the label exists by listing all labels
|
|
338
|
+
const labels = await this.listLabels(blogId);
|
|
339
|
+
const label = labels.items?.find(l => l.name === labelName);
|
|
340
|
+
|
|
341
|
+
if (!label) {
|
|
342
|
+
throw new Error(`Label ${labelName} not found`);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return label;
|
|
346
|
+
} catch (error) {
|
|
347
|
+
console.error(`Error fetching label ${labelName}:`, error);
|
|
348
|
+
throw error;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for config.ts
|
|
3
|
+
*
|
|
4
|
+
* config.ts reads process.env at module load time, so we must
|
|
5
|
+
* set env vars BEFORE importing. We use jest.isolateModules()
|
|
6
|
+
* to get a fresh module for each test.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Helper to load config with specific env vars
|
|
10
|
+
function loadConfig(env: Record<string, string>) {
|
|
11
|
+
// Clean all relevant env vars first
|
|
12
|
+
const keys = [
|
|
13
|
+
'MCP_MODE', 'MCP_HTTP_HOST', 'MCP_HTTP_PORT',
|
|
14
|
+
'BLOGGER_API_KEY', 'BLOGGER_MAX_RESULTS', 'BLOGGER_API_TIMEOUT',
|
|
15
|
+
'GOOGLE_CLIENT_ID', 'GOOGLE_CLIENT_SECRET', 'GOOGLE_REFRESH_TOKEN',
|
|
16
|
+
'LOG_LEVEL'
|
|
17
|
+
];
|
|
18
|
+
for (const key of keys) {
|
|
19
|
+
delete process.env[key];
|
|
20
|
+
}
|
|
21
|
+
// Set requested env vars
|
|
22
|
+
for (const [key, value] of Object.entries(env)) {
|
|
23
|
+
process.env[key] = value;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Force fresh import
|
|
27
|
+
let config: any;
|
|
28
|
+
jest.isolateModules(() => {
|
|
29
|
+
config = require('./config').config;
|
|
30
|
+
});
|
|
31
|
+
return config;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
jest.resetModules();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('config.ts', () => {
|
|
39
|
+
|
|
40
|
+
describe('defaults', () => {
|
|
41
|
+
it('should use default values when no env vars are set', () => {
|
|
42
|
+
const config = loadConfig({});
|
|
43
|
+
|
|
44
|
+
expect(config.mode).toBe('stdio');
|
|
45
|
+
expect(config.http.host).toBe('0.0.0.0');
|
|
46
|
+
expect(config.http.port).toBe(3000);
|
|
47
|
+
expect(config.blogger.apiKey).toBeUndefined();
|
|
48
|
+
expect(config.blogger.maxResults).toBe(10);
|
|
49
|
+
expect(config.blogger.timeout).toBe(30000);
|
|
50
|
+
expect(config.oauth2.clientId).toBeUndefined();
|
|
51
|
+
expect(config.oauth2.clientSecret).toBeUndefined();
|
|
52
|
+
expect(config.oauth2.refreshToken).toBeUndefined();
|
|
53
|
+
expect(config.logging.level).toBe('info');
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('mode', () => {
|
|
58
|
+
it('should read MCP_MODE from env', () => {
|
|
59
|
+
const config = loadConfig({ MCP_MODE: 'http' });
|
|
60
|
+
expect(config.mode).toBe('http');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should accept arbitrary mode values (no validation at config level)', () => {
|
|
64
|
+
const config = loadConfig({ MCP_MODE: 'invalid' });
|
|
65
|
+
expect(config.mode).toBe('invalid');
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('http', () => {
|
|
70
|
+
it('should read host and port from env', () => {
|
|
71
|
+
const config = loadConfig({ MCP_HTTP_HOST: '127.0.0.1', MCP_HTTP_PORT: '8080' });
|
|
72
|
+
expect(config.http.host).toBe('127.0.0.1');
|
|
73
|
+
expect(config.http.port).toBe(8080);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should return NaN for non-numeric port', () => {
|
|
77
|
+
const config = loadConfig({ MCP_HTTP_PORT: 'abc' });
|
|
78
|
+
expect(config.http.port).toBeNaN();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('blogger', () => {
|
|
83
|
+
it('should read API key from env', () => {
|
|
84
|
+
const config = loadConfig({ BLOGGER_API_KEY: 'test-key-123' });
|
|
85
|
+
expect(config.blogger.apiKey).toBe('test-key-123');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should parse maxResults and timeout as integers', () => {
|
|
89
|
+
const config = loadConfig({ BLOGGER_MAX_RESULTS: '25', BLOGGER_API_TIMEOUT: '60000' });
|
|
90
|
+
expect(config.blogger.maxResults).toBe(25);
|
|
91
|
+
expect(config.blogger.timeout).toBe(60000);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('oauth2', () => {
|
|
96
|
+
it('should read all OAuth2 vars from env', () => {
|
|
97
|
+
const config = loadConfig({
|
|
98
|
+
GOOGLE_CLIENT_ID: 'cid',
|
|
99
|
+
GOOGLE_CLIENT_SECRET: 'csec',
|
|
100
|
+
GOOGLE_REFRESH_TOKEN: 'rtok'
|
|
101
|
+
});
|
|
102
|
+
expect(config.oauth2.clientId).toBe('cid');
|
|
103
|
+
expect(config.oauth2.clientSecret).toBe('csec');
|
|
104
|
+
expect(config.oauth2.refreshToken).toBe('rtok');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should leave oauth2 fields undefined when partially set', () => {
|
|
108
|
+
const config = loadConfig({ GOOGLE_CLIENT_ID: 'cid' });
|
|
109
|
+
expect(config.oauth2.clientId).toBe('cid');
|
|
110
|
+
expect(config.oauth2.clientSecret).toBeUndefined();
|
|
111
|
+
expect(config.oauth2.refreshToken).toBeUndefined();
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('logging', () => {
|
|
116
|
+
it('should read LOG_LEVEL from env', () => {
|
|
117
|
+
const config = loadConfig({ LOG_LEVEL: 'debug' });
|
|
118
|
+
expect(config.logging.level).toBe('debug');
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
});
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// MCP server configuration for Blogger
|
|
2
|
+
export const config = {
|
|
3
|
+
// Server operating mode (stdio or http)
|
|
4
|
+
mode: process.env.MCP_MODE || 'stdio',
|
|
5
|
+
|
|
6
|
+
// HTTP mode configuration (if used)
|
|
7
|
+
http: {
|
|
8
|
+
host: process.env.MCP_HTTP_HOST || '0.0.0.0',
|
|
9
|
+
port: parseInt(process.env.MCP_HTTP_PORT || '3000', 10)
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
// Blogger API configuration
|
|
13
|
+
blogger: {
|
|
14
|
+
apiKey: process.env.BLOGGER_API_KEY,
|
|
15
|
+
// Default maximum number of results for list queries
|
|
16
|
+
maxResults: parseInt(process.env.BLOGGER_MAX_RESULTS || '10', 10),
|
|
17
|
+
// API request timeout in milliseconds
|
|
18
|
+
timeout: parseInt(process.env.BLOGGER_API_TIMEOUT || '30000', 10)
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
// OAuth2 configuration for authenticated operations (create, update, delete)
|
|
22
|
+
// If these variables are not set, the server runs in read-only mode (API key)
|
|
23
|
+
oauth2: {
|
|
24
|
+
clientId: process.env.GOOGLE_CLIENT_ID,
|
|
25
|
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
|
26
|
+
refreshToken: process.env.GOOGLE_REFRESH_TOKEN
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
// Logging configuration
|
|
30
|
+
logging: {
|
|
31
|
+
level: process.env.LOG_LEVEL || 'info'
|
|
32
|
+
}
|
|
33
|
+
};
|