@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/src/bloggerService.ts
CHANGED
|
@@ -1,33 +1,16 @@
|
|
|
1
1
|
import { google, blogger_v3 } from 'googleapis';
|
|
2
|
-
import {
|
|
2
|
+
import { BloggerPost } from './types';
|
|
3
3
|
import { config } from './config';
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
* Custom types to compensate for Blogger API limitations
|
|
7
|
-
*/
|
|
8
5
|
interface BloggerLabelList {
|
|
9
6
|
kind?: string;
|
|
10
|
-
items?:
|
|
7
|
+
items?: Array<{ name: string }>;
|
|
11
8
|
}
|
|
12
9
|
|
|
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
10
|
export class BloggerService {
|
|
25
11
|
private blogger: blogger_v3.Blogger;
|
|
26
12
|
private readonly isOAuth2: boolean;
|
|
27
13
|
|
|
28
|
-
/**
|
|
29
|
-
* Initializes the Blogger service with OAuth2 or API key
|
|
30
|
-
*/
|
|
31
14
|
constructor() {
|
|
32
15
|
const { oauth2 } = config;
|
|
33
16
|
const hasOAuth2 = !!(oauth2.clientId && oauth2.clientSecret && oauth2.refreshToken);
|
|
@@ -63,10 +46,6 @@ export class BloggerService {
|
|
|
63
46
|
}
|
|
64
47
|
}
|
|
65
48
|
|
|
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
49
|
private requireOAuth2(operation: string): void {
|
|
71
50
|
if (!this.isOAuth2) {
|
|
72
51
|
throw new Error(
|
|
@@ -77,11 +56,6 @@ export class BloggerService {
|
|
|
77
56
|
}
|
|
78
57
|
}
|
|
79
58
|
|
|
80
|
-
/**
|
|
81
|
-
* Lists all blogs for the authenticated user.
|
|
82
|
-
* Requires OAuth2 (blogs.listByUser with userId: 'self').
|
|
83
|
-
* @returns Blog list
|
|
84
|
-
*/
|
|
85
59
|
async listBlogs(): Promise<blogger_v3.Schema$BlogList> {
|
|
86
60
|
this.requireOAuth2('list_blogs');
|
|
87
61
|
try {
|
|
@@ -95,16 +69,9 @@ export class BloggerService {
|
|
|
95
69
|
}
|
|
96
70
|
}
|
|
97
71
|
|
|
98
|
-
/**
|
|
99
|
-
* Retrieves details of a specific blog
|
|
100
|
-
* @param blogId ID of the blog to retrieve
|
|
101
|
-
* @returns Blog details
|
|
102
|
-
*/
|
|
103
72
|
async getBlog(blogId: string): Promise<blogger_v3.Schema$Blog> {
|
|
104
73
|
try {
|
|
105
|
-
const response = await this.blogger.blogs.get({
|
|
106
|
-
blogId
|
|
107
|
-
});
|
|
74
|
+
const response = await this.blogger.blogs.get({ blogId });
|
|
108
75
|
return response.data;
|
|
109
76
|
} catch (error) {
|
|
110
77
|
console.error(`Error fetching blog ${blogId}:`, error);
|
|
@@ -112,16 +79,9 @@ export class BloggerService {
|
|
|
112
79
|
}
|
|
113
80
|
}
|
|
114
81
|
|
|
115
|
-
/**
|
|
116
|
-
* Retrieves a blog by its URL
|
|
117
|
-
* @param url Blog URL
|
|
118
|
-
* @returns Blog details
|
|
119
|
-
*/
|
|
120
82
|
async getBlogByUrl(url: string): Promise<blogger_v3.Schema$Blog> {
|
|
121
83
|
try {
|
|
122
|
-
const response = await this.blogger.blogs.getByUrl({
|
|
123
|
-
url
|
|
124
|
-
});
|
|
84
|
+
const response = await this.blogger.blogs.getByUrl({ url });
|
|
125
85
|
return response.data;
|
|
126
86
|
} catch (error) {
|
|
127
87
|
console.error(`Error fetching blog by URL ${url}:`, error);
|
|
@@ -129,33 +89,6 @@ export class BloggerService {
|
|
|
129
89
|
}
|
|
130
90
|
}
|
|
131
91
|
|
|
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
92
|
async listPosts(blogId: string, maxResults?: number): Promise<blogger_v3.Schema$PostList> {
|
|
160
93
|
try {
|
|
161
94
|
const response = await this.blogger.posts.list({
|
|
@@ -169,13 +102,6 @@ export class BloggerService {
|
|
|
169
102
|
}
|
|
170
103
|
}
|
|
171
104
|
|
|
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
105
|
async searchPosts(blogId: string, query: string, maxResults?: number): Promise<blogger_v3.Schema$PostList> {
|
|
180
106
|
try {
|
|
181
107
|
const response = await this.blogger.posts.search({
|
|
@@ -184,8 +110,6 @@ export class BloggerService {
|
|
|
184
110
|
fetchBodies: true
|
|
185
111
|
});
|
|
186
112
|
|
|
187
|
-
// The search endpoint does not support maxResults directly,
|
|
188
|
-
// so we truncate client-side if needed
|
|
189
113
|
const items = response.data.items || [];
|
|
190
114
|
const limit = maxResults || config.blogger.maxResults;
|
|
191
115
|
|
|
@@ -199,18 +123,9 @@ export class BloggerService {
|
|
|
199
123
|
}
|
|
200
124
|
}
|
|
201
125
|
|
|
202
|
-
/**
|
|
203
|
-
* Retrieves a specific post
|
|
204
|
-
* @param blogId Blog ID
|
|
205
|
-
* @param postId Post ID
|
|
206
|
-
* @returns Post details
|
|
207
|
-
*/
|
|
208
126
|
async getPost(blogId: string, postId: string): Promise<blogger_v3.Schema$Post> {
|
|
209
127
|
try {
|
|
210
|
-
const response = await this.blogger.posts.get({
|
|
211
|
-
blogId,
|
|
212
|
-
postId
|
|
213
|
-
});
|
|
128
|
+
const response = await this.blogger.posts.get({ blogId, postId });
|
|
214
129
|
return response.data;
|
|
215
130
|
} catch (error) {
|
|
216
131
|
console.error(`Error fetching post ${postId}:`, error);
|
|
@@ -218,20 +133,15 @@ export class BloggerService {
|
|
|
218
133
|
}
|
|
219
134
|
}
|
|
220
135
|
|
|
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
136
|
async createPost(blogId: string, postData: Partial<BloggerPost>): Promise<blogger_v3.Schema$Post> {
|
|
229
137
|
this.requireOAuth2('create_post');
|
|
230
138
|
try {
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
139
|
+
const requestBody: blogger_v3.Schema$Post = {
|
|
140
|
+
title: postData.title ?? undefined,
|
|
141
|
+
content: postData.content ?? undefined,
|
|
142
|
+
labels: postData.labels ?? undefined
|
|
143
|
+
};
|
|
144
|
+
const response = await this.blogger.posts.insert({ blogId, requestBody });
|
|
235
145
|
return response.data;
|
|
236
146
|
} catch (error) {
|
|
237
147
|
console.error(`Error creating post in blog ${blogId}:`, error);
|
|
@@ -239,29 +149,15 @@ export class BloggerService {
|
|
|
239
149
|
}
|
|
240
150
|
}
|
|
241
151
|
|
|
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
152
|
async updatePost(blogId: string, postId: string, postData: Partial<BloggerPost>): Promise<blogger_v3.Schema$Post> {
|
|
251
153
|
this.requireOAuth2('update_post');
|
|
252
154
|
try {
|
|
253
|
-
// Convert types to avoid compilation errors
|
|
254
155
|
const requestBody: blogger_v3.Schema$Post = {
|
|
255
|
-
title: postData.title,
|
|
256
|
-
content: postData.content,
|
|
257
|
-
labels: postData.labels
|
|
156
|
+
title: postData.title ?? undefined,
|
|
157
|
+
content: postData.content ?? undefined,
|
|
158
|
+
labels: postData.labels ?? undefined
|
|
258
159
|
};
|
|
259
|
-
|
|
260
|
-
const response = await this.blogger.posts.update({
|
|
261
|
-
blogId,
|
|
262
|
-
postId,
|
|
263
|
-
requestBody
|
|
264
|
-
});
|
|
160
|
+
const response = await this.blogger.posts.update({ blogId, postId, requestBody });
|
|
265
161
|
return response.data;
|
|
266
162
|
} catch (error) {
|
|
267
163
|
console.error(`Error updating post ${postId}:`, error);
|
|
@@ -269,79 +165,48 @@ export class BloggerService {
|
|
|
269
165
|
}
|
|
270
166
|
}
|
|
271
167
|
|
|
272
|
-
/**
|
|
273
|
-
* Deletes a post.
|
|
274
|
-
* Requires OAuth2.
|
|
275
|
-
* @param blogId Blog ID
|
|
276
|
-
* @param postId Post ID
|
|
277
|
-
* @returns Deletion status
|
|
278
|
-
*/
|
|
279
168
|
async deletePost(blogId: string, postId: string): Promise<void> {
|
|
280
169
|
this.requireOAuth2('delete_post');
|
|
281
170
|
try {
|
|
282
|
-
await this.blogger.posts.delete({
|
|
283
|
-
blogId,
|
|
284
|
-
postId
|
|
285
|
-
});
|
|
171
|
+
await this.blogger.posts.delete({ blogId, postId });
|
|
286
172
|
} catch (error) {
|
|
287
173
|
console.error(`Error deleting post ${postId}:`, error);
|
|
288
174
|
throw error;
|
|
289
175
|
}
|
|
290
176
|
}
|
|
291
177
|
|
|
292
|
-
/**
|
|
293
|
-
* Lists labels from a blog
|
|
294
|
-
* @param blogId Blog ID
|
|
295
|
-
* @returns Label list
|
|
296
|
-
*/
|
|
297
178
|
async listLabels(blogId: string): Promise<BloggerLabelList> {
|
|
298
179
|
try {
|
|
299
|
-
// The Blogger API does not provide a direct endpoint to list labels
|
|
300
|
-
// We fetch all posts and extract unique labels
|
|
301
180
|
const response = await this.blogger.posts.list({
|
|
302
181
|
blogId,
|
|
303
|
-
maxResults: 50
|
|
182
|
+
maxResults: 50
|
|
304
183
|
});
|
|
305
|
-
|
|
184
|
+
|
|
306
185
|
const posts = response.data.items || [];
|
|
307
186
|
const labelSet = new Set<string>();
|
|
308
|
-
|
|
309
|
-
// Extract all unique labels from posts
|
|
187
|
+
|
|
310
188
|
posts.forEach(post => {
|
|
311
|
-
|
|
312
|
-
postLabels.forEach(label => labelSet.add(label));
|
|
189
|
+
(post.labels || []).forEach(label => labelSet.add(label));
|
|
313
190
|
});
|
|
314
|
-
|
|
315
|
-
// Convert to expected format
|
|
191
|
+
|
|
316
192
|
const labels = Array.from(labelSet).map(name => ({ name }));
|
|
317
|
-
|
|
318
|
-
return {
|
|
319
|
-
kind: 'blogger#labelList',
|
|
320
|
-
items: labels
|
|
321
|
-
};
|
|
193
|
+
|
|
194
|
+
return { kind: 'blogger#labelList', items: labels };
|
|
322
195
|
} catch (error) {
|
|
323
196
|
console.error(`Error fetching labels for blog ${blogId}:`, error);
|
|
324
197
|
throw error;
|
|
325
198
|
}
|
|
326
199
|
}
|
|
327
200
|
|
|
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> {
|
|
201
|
+
async getLabel(blogId: string, labelName: string): Promise<{ name: string }> {
|
|
335
202
|
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
203
|
const labels = await this.listLabels(blogId);
|
|
339
204
|
const label = labels.items?.find(l => l.name === labelName);
|
|
340
|
-
|
|
205
|
+
|
|
341
206
|
if (!label) {
|
|
342
207
|
throw new Error(`Label ${labelName} not found`);
|
|
343
208
|
}
|
|
344
|
-
|
|
209
|
+
|
|
345
210
|
return label;
|
|
346
211
|
} catch (error) {
|
|
347
212
|
console.error(`Error fetching label ${labelName}:`, error);
|
package/src/config.test.ts
CHANGED
|
@@ -1,29 +1,18 @@
|
|
|
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
1
|
function loadConfig(env: Record<string, string>) {
|
|
11
|
-
// Clean all relevant env vars first
|
|
12
2
|
const keys = [
|
|
13
3
|
'MCP_MODE', 'MCP_HTTP_HOST', 'MCP_HTTP_PORT',
|
|
14
4
|
'BLOGGER_API_KEY', 'BLOGGER_MAX_RESULTS', 'BLOGGER_API_TIMEOUT',
|
|
15
5
|
'GOOGLE_CLIENT_ID', 'GOOGLE_CLIENT_SECRET', 'GOOGLE_REFRESH_TOKEN',
|
|
16
|
-
'LOG_LEVEL'
|
|
6
|
+
'LOG_LEVEL', 'UI_PORT'
|
|
17
7
|
];
|
|
18
8
|
for (const key of keys) {
|
|
19
9
|
delete process.env[key];
|
|
20
10
|
}
|
|
21
|
-
// Set requested env vars
|
|
22
11
|
for (const [key, value] of Object.entries(env)) {
|
|
23
12
|
process.env[key] = value;
|
|
24
13
|
}
|
|
25
14
|
|
|
26
|
-
//
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
27
16
|
let config: any;
|
|
28
17
|
jest.isolateModules(() => {
|
|
29
18
|
config = require('./config').config;
|
|
@@ -51,6 +40,7 @@ describe('config.ts', () => {
|
|
|
51
40
|
expect(config.oauth2.clientSecret).toBeUndefined();
|
|
52
41
|
expect(config.oauth2.refreshToken).toBeUndefined();
|
|
53
42
|
expect(config.logging.level).toBe('info');
|
|
43
|
+
expect(config.ui.port).toBe(0);
|
|
54
44
|
});
|
|
55
45
|
});
|
|
56
46
|
|
|
@@ -59,11 +49,6 @@ describe('config.ts', () => {
|
|
|
59
49
|
const config = loadConfig({ MCP_MODE: 'http' });
|
|
60
50
|
expect(config.mode).toBe('http');
|
|
61
51
|
});
|
|
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
52
|
});
|
|
68
53
|
|
|
69
54
|
describe('http', () => {
|
|
@@ -72,10 +57,22 @@ describe('config.ts', () => {
|
|
|
72
57
|
expect(config.http.host).toBe('127.0.0.1');
|
|
73
58
|
expect(config.http.port).toBe(8080);
|
|
74
59
|
});
|
|
60
|
+
});
|
|
75
61
|
|
|
76
|
-
|
|
62
|
+
describe('safeInt (NaN protection)', () => {
|
|
63
|
+
it('should return default for non-numeric port', () => {
|
|
77
64
|
const config = loadConfig({ MCP_HTTP_PORT: 'abc' });
|
|
78
|
-
expect(config.http.port).
|
|
65
|
+
expect(config.http.port).toBe(3000);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should return default for non-numeric maxResults', () => {
|
|
69
|
+
const config = loadConfig({ BLOGGER_MAX_RESULTS: 'notanumber' });
|
|
70
|
+
expect(config.blogger.maxResults).toBe(10);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should return default for non-numeric timeout', () => {
|
|
74
|
+
const config = loadConfig({ BLOGGER_API_TIMEOUT: 'xyz' });
|
|
75
|
+
expect(config.blogger.timeout).toBe(30000);
|
|
79
76
|
});
|
|
80
77
|
});
|
|
81
78
|
|
|
@@ -118,4 +115,21 @@ describe('config.ts', () => {
|
|
|
118
115
|
expect(config.logging.level).toBe('debug');
|
|
119
116
|
});
|
|
120
117
|
});
|
|
118
|
+
|
|
119
|
+
describe('ui', () => {
|
|
120
|
+
it('should default to port 0 (disabled)', () => {
|
|
121
|
+
const config = loadConfig({});
|
|
122
|
+
expect(config.ui.port).toBe(0);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should read UI_PORT from env', () => {
|
|
126
|
+
const config = loadConfig({ UI_PORT: '4000' });
|
|
127
|
+
expect(config.ui.port).toBe(4000);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should return default 0 for non-numeric UI_PORT', () => {
|
|
131
|
+
const config = loadConfig({ UI_PORT: 'abc' });
|
|
132
|
+
expect(config.ui.port).toBe(0);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
121
135
|
});
|
package/src/config.ts
CHANGED
|
@@ -1,33 +1,34 @@
|
|
|
1
|
-
|
|
1
|
+
function safeInt(value: string | undefined, defaultValue: number): number {
|
|
2
|
+
if (!value) return defaultValue;
|
|
3
|
+
const parsed = parseInt(value, 10);
|
|
4
|
+
return Number.isNaN(parsed) ? defaultValue : parsed;
|
|
5
|
+
}
|
|
6
|
+
|
|
2
7
|
export const config = {
|
|
3
|
-
// Server operating mode (stdio or http)
|
|
4
8
|
mode: process.env.MCP_MODE || 'stdio',
|
|
5
|
-
|
|
6
|
-
// HTTP mode configuration (if used)
|
|
9
|
+
|
|
7
10
|
http: {
|
|
8
11
|
host: process.env.MCP_HTTP_HOST || '0.0.0.0',
|
|
9
|
-
port:
|
|
12
|
+
port: safeInt(process.env.MCP_HTTP_PORT, 3000)
|
|
10
13
|
},
|
|
11
|
-
|
|
12
|
-
// Blogger API configuration
|
|
14
|
+
|
|
13
15
|
blogger: {
|
|
14
16
|
apiKey: process.env.BLOGGER_API_KEY,
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
// API request timeout in milliseconds
|
|
18
|
-
timeout: parseInt(process.env.BLOGGER_API_TIMEOUT || '30000', 10)
|
|
17
|
+
maxResults: safeInt(process.env.BLOGGER_MAX_RESULTS, 10),
|
|
18
|
+
timeout: safeInt(process.env.BLOGGER_API_TIMEOUT, 30000)
|
|
19
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)
|
|
20
|
+
|
|
23
21
|
oauth2: {
|
|
24
22
|
clientId: process.env.GOOGLE_CLIENT_ID,
|
|
25
23
|
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
|
26
24
|
refreshToken: process.env.GOOGLE_REFRESH_TOKEN
|
|
27
25
|
},
|
|
28
|
-
|
|
29
|
-
// Logging configuration
|
|
26
|
+
|
|
30
27
|
logging: {
|
|
31
28
|
level: process.env.LOG_LEVEL || 'info'
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
ui: {
|
|
32
|
+
port: safeInt(process.env.UI_PORT, 0)
|
|
32
33
|
}
|
|
33
34
|
};
|