@mseep/mcp-agent-social 1.1.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.
Files changed (165) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +154 -0
  3. package/bin/mcp-agent-social +30 -0
  4. package/dist/api-client.d.ts +31 -0
  5. package/dist/api-client.d.ts.map +1 -0
  6. package/dist/api-client.js +212 -0
  7. package/dist/api-client.js.map +1 -0
  8. package/dist/config.d.ts +19 -0
  9. package/dist/config.d.ts.map +1 -0
  10. package/dist/config.js +79 -0
  11. package/dist/config.js.map +1 -0
  12. package/dist/hooks/index.d.ts +38 -0
  13. package/dist/hooks/index.d.ts.map +1 -0
  14. package/dist/hooks/index.js +253 -0
  15. package/dist/hooks/index.js.map +1 -0
  16. package/dist/hooks/types.d.ts +35 -0
  17. package/dist/hooks/types.d.ts.map +1 -0
  18. package/dist/hooks/types.js +4 -0
  19. package/dist/hooks/types.js.map +1 -0
  20. package/dist/http-server.d.ts +38 -0
  21. package/dist/http-server.d.ts.map +1 -0
  22. package/dist/http-server.js +210 -0
  23. package/dist/http-server.js.map +1 -0
  24. package/dist/index.d.ts +2 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +186 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/logger.d.ts +44 -0
  29. package/dist/logger.d.ts.map +1 -0
  30. package/dist/logger.js +281 -0
  31. package/dist/logger.js.map +1 -0
  32. package/dist/metrics.d.ts +47 -0
  33. package/dist/metrics.d.ts.map +1 -0
  34. package/dist/metrics.js +178 -0
  35. package/dist/metrics.js.map +1 -0
  36. package/dist/middleware/error-handler.d.ts +74 -0
  37. package/dist/middleware/error-handler.d.ts.map +1 -0
  38. package/dist/middleware/error-handler.js +218 -0
  39. package/dist/middleware/error-handler.js.map +1 -0
  40. package/dist/middleware/index.d.ts +55 -0
  41. package/dist/middleware/index.d.ts.map +1 -0
  42. package/dist/middleware/index.js +91 -0
  43. package/dist/middleware/index.js.map +1 -0
  44. package/dist/middleware/timeout.d.ts +52 -0
  45. package/dist/middleware/timeout.d.ts.map +1 -0
  46. package/dist/middleware/timeout.js +189 -0
  47. package/dist/middleware/timeout.js.map +1 -0
  48. package/dist/middleware/validator.d.ts +25 -0
  49. package/dist/middleware/validator.d.ts.map +1 -0
  50. package/dist/middleware/validator.js +186 -0
  51. package/dist/middleware/validator.js.map +1 -0
  52. package/dist/prompts/analyze.d.ts +46 -0
  53. package/dist/prompts/analyze.d.ts.map +1 -0
  54. package/dist/prompts/analyze.js +351 -0
  55. package/dist/prompts/analyze.js.map +1 -0
  56. package/dist/prompts/generate.d.ts +48 -0
  57. package/dist/prompts/generate.d.ts.map +1 -0
  58. package/dist/prompts/generate.js +177 -0
  59. package/dist/prompts/generate.js.map +1 -0
  60. package/dist/prompts/index.d.ts +23 -0
  61. package/dist/prompts/index.d.ts.map +1 -0
  62. package/dist/prompts/index.js +69 -0
  63. package/dist/prompts/index.js.map +1 -0
  64. package/dist/prompts/summarize.d.ts +32 -0
  65. package/dist/prompts/summarize.d.ts.map +1 -0
  66. package/dist/prompts/summarize.js +182 -0
  67. package/dist/prompts/summarize.js.map +1 -0
  68. package/dist/prompts/types.d.ts +34 -0
  69. package/dist/prompts/types.d.ts.map +1 -0
  70. package/dist/prompts/types.js +24 -0
  71. package/dist/prompts/types.js.map +1 -0
  72. package/dist/resources/agents.d.ts +17 -0
  73. package/dist/resources/agents.d.ts.map +1 -0
  74. package/dist/resources/agents.js +139 -0
  75. package/dist/resources/agents.js.map +1 -0
  76. package/dist/resources/feed.d.ts +19 -0
  77. package/dist/resources/feed.d.ts.map +1 -0
  78. package/dist/resources/feed.js +138 -0
  79. package/dist/resources/feed.js.map +1 -0
  80. package/dist/resources/index.d.ts +19 -0
  81. package/dist/resources/index.d.ts.map +1 -0
  82. package/dist/resources/index.js +146 -0
  83. package/dist/resources/index.js.map +1 -0
  84. package/dist/resources/posts.d.ts +17 -0
  85. package/dist/resources/posts.d.ts.map +1 -0
  86. package/dist/resources/posts.js +151 -0
  87. package/dist/resources/posts.js.map +1 -0
  88. package/dist/resources/types.d.ts +91 -0
  89. package/dist/resources/types.d.ts.map +1 -0
  90. package/dist/resources/types.js +12 -0
  91. package/dist/resources/types.js.map +1 -0
  92. package/dist/roots/index.d.ts +43 -0
  93. package/dist/roots/index.d.ts.map +1 -0
  94. package/dist/roots/index.js +131 -0
  95. package/dist/roots/index.js.map +1 -0
  96. package/dist/roots/types.d.ts +31 -0
  97. package/dist/roots/types.d.ts.map +1 -0
  98. package/dist/roots/types.js +4 -0
  99. package/dist/roots/types.js.map +1 -0
  100. package/dist/session-manager.d.ts +50 -0
  101. package/dist/session-manager.d.ts.map +1 -0
  102. package/dist/session-manager.js +127 -0
  103. package/dist/session-manager.js.map +1 -0
  104. package/dist/tools/create-post.d.ts +45 -0
  105. package/dist/tools/create-post.d.ts.map +1 -0
  106. package/dist/tools/create-post.js +119 -0
  107. package/dist/tools/create-post.js.map +1 -0
  108. package/dist/tools/index.d.ts +13 -0
  109. package/dist/tools/index.d.ts.map +1 -0
  110. package/dist/tools/index.js +44 -0
  111. package/dist/tools/index.js.map +1 -0
  112. package/dist/tools/login.d.ts +35 -0
  113. package/dist/tools/login.d.ts.map +1 -0
  114. package/dist/tools/login.js +132 -0
  115. package/dist/tools/login.js.map +1 -0
  116. package/dist/tools/read-posts.d.ts +48 -0
  117. package/dist/tools/read-posts.d.ts.map +1 -0
  118. package/dist/tools/read-posts.js +93 -0
  119. package/dist/tools/read-posts.js.map +1 -0
  120. package/dist/types.d.ts +88 -0
  121. package/dist/types.d.ts.map +1 -0
  122. package/dist/types.js +4 -0
  123. package/dist/types.js.map +1 -0
  124. package/dist/utils/json.d.ts +13 -0
  125. package/dist/utils/json.d.ts.map +1 -0
  126. package/dist/utils/json.js +48 -0
  127. package/dist/utils/json.js.map +1 -0
  128. package/dist/validation.d.ts +58 -0
  129. package/dist/validation.d.ts.map +1 -0
  130. package/dist/validation.js +223 -0
  131. package/dist/validation.js.map +1 -0
  132. package/package.json +70 -0
  133. package/src/api-client.ts +292 -0
  134. package/src/config.ts +92 -0
  135. package/src/hooks/index.ts +304 -0
  136. package/src/hooks/types.ts +44 -0
  137. package/src/http-server.ts +243 -0
  138. package/src/index.ts +213 -0
  139. package/src/logger.ts +326 -0
  140. package/src/metrics.ts +235 -0
  141. package/src/middleware/error-handler.ts +252 -0
  142. package/src/middleware/index.ts +112 -0
  143. package/src/middleware/timeout.ts +216 -0
  144. package/src/middleware/validator.ts +216 -0
  145. package/src/prompts/analyze.ts +404 -0
  146. package/src/prompts/generate.ts +217 -0
  147. package/src/prompts/index.ts +121 -0
  148. package/src/prompts/summarize.ts +217 -0
  149. package/src/prompts/types.ts +44 -0
  150. package/src/resources/agents.ts +165 -0
  151. package/src/resources/feed.ts +169 -0
  152. package/src/resources/index.ts +210 -0
  153. package/src/resources/posts.ts +179 -0
  154. package/src/resources/types.ts +104 -0
  155. package/src/roots/index.ts +166 -0
  156. package/src/roots/types.ts +36 -0
  157. package/src/session-manager.ts +149 -0
  158. package/src/tools/create-post.ts +154 -0
  159. package/src/tools/index.ts +70 -0
  160. package/src/tools/login.ts +169 -0
  161. package/src/tools/read-posts.ts +120 -0
  162. package/src/types.ts +107 -0
  163. package/src/utils/json.ts +46 -0
  164. package/src/validation.ts +322 -0
  165. package/tsconfig.json +22 -0
@@ -0,0 +1,223 @@
1
+ // ABOUTME: JSON Schema validation utilities for MCP tools
2
+ // ABOUTME: Provides runtime validation for tool inputs
3
+ export class ValidationResult {
4
+ isValid;
5
+ errors;
6
+ data;
7
+ constructor(isValid, errors = [], data) {
8
+ this.isValid = isValid;
9
+ this.errors = errors;
10
+ this.data = data;
11
+ }
12
+ static success(data) {
13
+ return new ValidationResult(true, [], data);
14
+ }
15
+ static failure(errors) {
16
+ return new ValidationResult(false, errors);
17
+ }
18
+ }
19
+ export function validateString(value, field, options = {}) {
20
+ const errors = [];
21
+ if (options.required && (value === undefined || value === null)) {
22
+ if (field === 'content') {
23
+ errors.push({ field, message: 'Content must not be empty' });
24
+ }
25
+ else {
26
+ errors.push({ field, message: `${field} is required` });
27
+ }
28
+ return errors;
29
+ }
30
+ if (value !== undefined && value !== null) {
31
+ if (typeof value !== 'string') {
32
+ errors.push({ field, message: `${field} must be a string` });
33
+ return errors;
34
+ }
35
+ // For content validation, check if trimmed string is empty
36
+ if (field === 'content' && options.required && value.trim().length === 0) {
37
+ errors.push({ field, message: 'Content must not be empty' });
38
+ return errors;
39
+ }
40
+ if (options.minLength && value.length < options.minLength) {
41
+ if (field === 'content') {
42
+ errors.push({ field, message: 'Content must not be empty' });
43
+ }
44
+ else {
45
+ errors.push({
46
+ field,
47
+ message: `${field} must be at least ${options.minLength} characters`,
48
+ });
49
+ }
50
+ }
51
+ if (options.maxLength && value.length > options.maxLength) {
52
+ errors.push({ field, message: `${field} must be at most ${options.maxLength} characters` });
53
+ }
54
+ }
55
+ return errors;
56
+ }
57
+ export function validateNumber(value, field, options = {}) {
58
+ const errors = [];
59
+ if (options.required && (value === undefined || value === null)) {
60
+ errors.push({ field, message: `${field} is required` });
61
+ return errors;
62
+ }
63
+ if (value !== undefined && value !== null) {
64
+ if (typeof value !== 'number' || Number.isNaN(value)) {
65
+ errors.push({ field, message: `${field} must be a number` });
66
+ return errors;
67
+ }
68
+ if (options.min !== undefined && value < options.min) {
69
+ errors.push({ field, message: `${field} must be at least ${options.min}` });
70
+ }
71
+ if (options.max !== undefined && value > options.max) {
72
+ errors.push({ field, message: `${field} must be at most ${options.max}` });
73
+ }
74
+ }
75
+ return errors;
76
+ }
77
+ export function validateArray(value, field, options = {}) {
78
+ const errors = [];
79
+ if (options.required && (value === undefined || value === null)) {
80
+ errors.push({ field, message: `${field} is required` });
81
+ return errors;
82
+ }
83
+ if (value !== undefined && value !== null) {
84
+ if (!Array.isArray(value)) {
85
+ errors.push({ field, message: `${field} must be an array` });
86
+ return errors;
87
+ }
88
+ if (options.itemValidator) {
89
+ value.forEach((item, index) => {
90
+ const itemErrors = options.itemValidator?.(item, index);
91
+ if (itemErrors) {
92
+ errors.push(...itemErrors.map((err) => ({
93
+ field: `${field}[${index}].${err.field}`,
94
+ message: err.message,
95
+ })));
96
+ }
97
+ });
98
+ }
99
+ }
100
+ return errors;
101
+ }
102
+ // Helper to trim string values consistently
103
+ function trimStringValue(value) {
104
+ if (value === undefined || value === null) {
105
+ return undefined;
106
+ }
107
+ if (typeof value === 'string') {
108
+ const trimmed = value.trim();
109
+ return trimmed.length > 0 ? trimmed : undefined;
110
+ }
111
+ // Only accept primitive types that can be meaningfully converted to strings
112
+ if (typeof value === 'number' || typeof value === 'boolean') {
113
+ return String(value);
114
+ }
115
+ // Reject objects, arrays, functions, etc.
116
+ return undefined;
117
+ }
118
+ // Login tool validation
119
+ export function validateLoginInput(input) {
120
+ const errors = [];
121
+ // Special handling for agent_name
122
+ if (input.agent_name === undefined || input.agent_name === null) {
123
+ errors.push({ field: 'agent_name', message: 'Agent name must not be empty' });
124
+ }
125
+ else if (typeof input.agent_name !== 'string') {
126
+ errors.push({ field: 'agent_name', message: 'Agent name must be a string' });
127
+ }
128
+ else if (input.agent_name.trim().length === 0) {
129
+ errors.push({ field: 'agent_name', message: 'Agent name must not be empty' });
130
+ }
131
+ if (errors.length > 0) {
132
+ return ValidationResult.failure(errors);
133
+ }
134
+ const agentName = trimStringValue(input.agent_name);
135
+ if (!agentName) {
136
+ return ValidationResult.failure([
137
+ { field: 'agent_name', message: 'Agent name cannot be empty' },
138
+ ]);
139
+ }
140
+ return ValidationResult.success({
141
+ agent_name: agentName,
142
+ });
143
+ }
144
+ // Read posts tool validation
145
+ export function validateReadPostsInput(input) {
146
+ const errors = [];
147
+ // Parse and validate numeric values
148
+ const limit = typeof input.limit === 'number'
149
+ ? input.limit
150
+ : typeof input.limit === 'string'
151
+ ? Number.parseInt(input.limit, 10)
152
+ : 10;
153
+ const offset = typeof input.offset === 'number'
154
+ ? input.offset
155
+ : typeof input.offset === 'string'
156
+ ? Number.parseInt(input.offset, 10)
157
+ : 0;
158
+ // Apply defaults and trim string values
159
+ const data = {
160
+ limit: Number.isNaN(limit) ? 10 : limit,
161
+ offset: Number.isNaN(offset) ? 0 : offset,
162
+ agent_filter: trimStringValue(input.agent_filter),
163
+ tag_filter: trimStringValue(input.tag_filter),
164
+ thread_id: trimStringValue(input.thread_id),
165
+ };
166
+ errors.push(...validateNumber(data.limit, 'limit', { min: 1, max: 100 }));
167
+ errors.push(...validateNumber(data.offset, 'offset', { min: 0 }));
168
+ // Check for empty string filters (before trimming converted them to undefined)
169
+ if (input.agent_filter !== undefined &&
170
+ input.agent_filter !== null &&
171
+ typeof input.agent_filter === 'string' &&
172
+ input.agent_filter.trim() === '') {
173
+ errors.push({ field: 'agent_filter', message: 'agent_filter cannot be empty' });
174
+ }
175
+ if (input.tag_filter !== undefined &&
176
+ input.tag_filter !== null &&
177
+ typeof input.tag_filter === 'string' &&
178
+ input.tag_filter.trim() === '') {
179
+ errors.push({ field: 'tag_filter', message: 'tag_filter cannot be empty' });
180
+ }
181
+ if (input.thread_id !== undefined &&
182
+ input.thread_id !== null &&
183
+ typeof input.thread_id === 'string' &&
184
+ input.thread_id.trim() === '') {
185
+ errors.push({ field: 'thread_id', message: 'thread_id cannot be empty' });
186
+ }
187
+ if (errors.length > 0) {
188
+ return ValidationResult.failure(errors);
189
+ }
190
+ return ValidationResult.success(data);
191
+ }
192
+ // Create post tool validation
193
+ export function validateCreatePostInput(input) {
194
+ const errors = [];
195
+ errors.push(...validateString(input.content, 'content', {
196
+ required: true,
197
+ minLength: 1,
198
+ }));
199
+ errors.push(...validateString(input.parent_post_id, 'parent_post_id'));
200
+ if (input.tags !== undefined) {
201
+ errors.push(...validateArray(input.tags, 'tags', {
202
+ itemValidator: (item, _index) => validateString(item, 'item', {}),
203
+ }));
204
+ }
205
+ if (errors.length > 0) {
206
+ return ValidationResult.failure(errors);
207
+ }
208
+ // Filter and trim tags consistently
209
+ const rawTags = Array.isArray(input.tags) ? input.tags : [];
210
+ const filteredTags = rawTags
211
+ .map((tag) => trimStringValue(tag))
212
+ .filter((tag) => tag !== undefined);
213
+ const content = trimStringValue(input.content);
214
+ if (!content) {
215
+ return ValidationResult.failure([{ field: 'content', message: 'Content cannot be empty' }]);
216
+ }
217
+ return ValidationResult.success({
218
+ content,
219
+ tags: filteredTags,
220
+ parent_post_id: trimStringValue(input.parent_post_id),
221
+ });
222
+ }
223
+ //# sourceMappingURL=validation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation.js","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "@mseep/mcp-agent-social",
3
+ "version": "1.1.0",
4
+ "description": "MCP server for agent social media platform",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "mcp-agent-social": "./bin/mcp-agent-social"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "start": "node dist/index.js",
12
+ "dev": "tsx watch src/index.ts",
13
+ "test": "NODE_OPTIONS=\"--experimental-vm-modules\" jest",
14
+ "test:coverage": "NODE_ENV=test NODE_OPTIONS=\"--experimental-vm-modules\" jest --coverage",
15
+ "test:watch": "NODE_OPTIONS=\"--experimental-vm-modules\" jest --watch",
16
+ "test:integration": "./scripts/test-mcp-integration.sh",
17
+ "test:mcp-sse": "NODE_OPTIONS=\"--experimental-vm-modules\" jest tests/integration/mcp-sse-mocked.test.ts",
18
+ "test:mcp-sse:old": "NODE_OPTIONS=\"--experimental-vm-modules\" jest tests/integration/mcp-sse.test.ts",
19
+ "test:mcp-sse:full": "TEST_SERVER_AUTO_START=true NODE_OPTIONS=\"--experimental-vm-modules\" jest tests/integration/mcp-sse.test.ts",
20
+ "start:http": "MCP_TRANSPORT=http MCP_HTTP_PORT=3000 npm start",
21
+ "lint": "biome check .",
22
+ "lint:fix": "biome check --write .",
23
+ "format": "biome format --write .",
24
+ "typecheck": "tsc --noEmit",
25
+ "mcp-probe": "./scripts/mcp-probe-tests.sh",
26
+ "mcp-probe:validate": "mcp-probe validate --stdio 'node' --args 'dist/index.js' --working-dir '$PWD'",
27
+ "mcp-probe:ci": "node scripts/ci-mcp-test.js",
28
+ "prepublishOnly": "npm run build && npm test"
29
+ },
30
+ "keywords": [
31
+ "mcp",
32
+ "model-context-protocol",
33
+ "social-media",
34
+ "agents",
35
+ "mseep",
36
+ "mcp-server"
37
+ ],
38
+ "author": "",
39
+ "license": "MIT",
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "https://github.com/2389-research/mcp-socialmedia.git"
43
+ },
44
+ "type": "module",
45
+ "files": [
46
+ "dist/",
47
+ "bin/",
48
+ "src/",
49
+ "tsconfig.json",
50
+ "README.md"
51
+ ],
52
+ "dependencies": {
53
+ "@modelcontextprotocol/sdk": "^1.12.1",
54
+ "@types/node-fetch": "^2.6.12",
55
+ "dotenv": "^16.5.0",
56
+ "node-fetch": "^3.3.2",
57
+ "typescript": "^5.8.3",
58
+ "zod": "^3.25.49"
59
+ },
60
+ "devDependencies": {
61
+ "@biomejs/biome": "^1.9.4",
62
+ "@types/jest": "^29.5.14",
63
+ "@types/node": "^22.15.29",
64
+ "jest": "^29.7.0",
65
+ "jest-junit": "^16.0.0",
66
+ "ts-jest": "^29.3.4",
67
+ "tsx": "^4.19.4"
68
+ },
69
+ "publisher": "mseep"
70
+ }
@@ -0,0 +1,292 @@
1
+ // ABOUTME: HTTP client for communicating with the external social media API
2
+ // ABOUTME: Handles authentication, error handling, and typed responses
3
+
4
+ import fetch, { type RequestInit, type Response } from 'node-fetch';
5
+
6
+ // Type for the fetch function to enable mocking in tests
7
+ export type FetchFunction = typeof fetch;
8
+ import { config } from './config.js';
9
+ import { logger } from './logger.js';
10
+ import {
11
+ McpAuthenticationError,
12
+ McpMethodNotFoundError,
13
+ McpRateLimitError,
14
+ McpTimeoutError,
15
+ } from './middleware/error-handler.js';
16
+ import type { PostData, PostQueryOptions, PostResponse, PostsResponse } from './types.js';
17
+
18
+ // Remote API response types
19
+ interface RemotePost {
20
+ postId: string;
21
+ author: string;
22
+ content: string;
23
+ tags?: string[];
24
+ createdAt?: {
25
+ _seconds: number;
26
+ };
27
+ parentPostId?: string;
28
+ }
29
+
30
+ interface RemotePostsResponse {
31
+ posts: RemotePost[];
32
+ totalCount: number;
33
+ nextOffset?: string;
34
+ }
35
+
36
+ interface RemotePostResponse {
37
+ postId: string;
38
+ author: string;
39
+ content: string;
40
+ tags?: string[];
41
+ createdAt?: {
42
+ _seconds: number;
43
+ };
44
+ parentPostId?: string;
45
+ }
46
+
47
+ export interface IApiClient {
48
+ fetchPosts(teamName: string, options?: PostQueryOptions): Promise<PostsResponse>;
49
+ createPost(teamName: string, postData: PostData): Promise<PostResponse>;
50
+ }
51
+
52
+ export class ApiClient implements IApiClient {
53
+ private baseUrl: string;
54
+ private apiKey: string;
55
+ private timeout: number;
56
+ private fetchFn: FetchFunction;
57
+
58
+ constructor(
59
+ baseUrl: string = config.socialApiBaseUrl,
60
+ apiKey: string = config.socialApiKey,
61
+ timeout: number = config.apiTimeout,
62
+ fetchFn: FetchFunction = fetch,
63
+ ) {
64
+ this.baseUrl = baseUrl.replace(/\/$/, ''); // Remove trailing slash
65
+ this.apiKey = apiKey;
66
+ this.timeout = timeout;
67
+ this.fetchFn = fetchFn;
68
+
69
+ logger.debug('ApiClient initialized', {
70
+ baseUrl: this.baseUrl,
71
+ timeout: this.timeout,
72
+ hasApiKey: !!this.apiKey,
73
+ });
74
+ }
75
+
76
+ /**
77
+ * Fetch posts from the API
78
+ */
79
+ async fetchPosts(teamName: string, options?: PostQueryOptions): Promise<PostsResponse> {
80
+ const params = new URLSearchParams();
81
+
82
+ if (options?.limit !== undefined) {
83
+ params.append('limit', options.limit.toString());
84
+ }
85
+ // Remote API uses cursor-based pagination, not numeric offset
86
+ // For now, we'll ignore the offset parameter since the remote API doesn't support it
87
+ // TODO: Implement proper cursor-based pagination mapping
88
+ // Note: remote API may not support agent/tag filters - these params might be ignored
89
+ if (options?.agent_filter) {
90
+ params.append('agent', options.agent_filter);
91
+ }
92
+ if (options?.tag_filter) {
93
+ params.append('tag', options.tag_filter);
94
+ }
95
+ if (options?.thread_id) {
96
+ params.append('thread_id', options.thread_id);
97
+ }
98
+
99
+ const queryString = params.toString();
100
+ const url = `${this.baseUrl}/teams/${encodeURIComponent(teamName)}/posts${
101
+ queryString ? `?${queryString}` : ''
102
+ }`;
103
+
104
+ logger.debug('Fetching posts', {
105
+ teamName,
106
+ queryParams: Object.fromEntries(params),
107
+ url,
108
+ });
109
+
110
+ const response = await this.makeRequest('GET', url);
111
+ const remoteResponse = response as RemotePostsResponse;
112
+
113
+ // Validate remote response
114
+ if (!remoteResponse.posts || !Array.isArray(remoteResponse.posts)) {
115
+ throw new Error('Invalid API response: posts array missing or malformed');
116
+ }
117
+
118
+ // Adapt remote response to our schema
119
+ const adaptedPosts = remoteResponse.posts
120
+ .filter((post: RemotePost) => {
121
+ if (!post.postId || !post.author || !post.content) {
122
+ logger.warn('Skipping malformed post', { post });
123
+ return false;
124
+ }
125
+ return true;
126
+ })
127
+ .map((post: RemotePost) => ({
128
+ id: post.postId,
129
+ author_name: post.author,
130
+ content: post.content,
131
+ tags: post.tags || [],
132
+ timestamp: post.createdAt?._seconds
133
+ ? new Date(post.createdAt._seconds * 1000).toISOString()
134
+ : new Date().toISOString(),
135
+ parent_post_id: post.parentPostId || undefined,
136
+ team_name: teamName,
137
+ }));
138
+
139
+ const adaptedResponse: PostsResponse = {
140
+ posts: adaptedPosts,
141
+ total: adaptedPosts.length, // Remote API doesn't provide total, estimate from current page
142
+ has_more: Boolean(remoteResponse.nextOffset),
143
+ };
144
+
145
+ return adaptedResponse;
146
+ }
147
+
148
+ /**
149
+ * Create a new post
150
+ */
151
+ async createPost(teamName: string, postData: PostData): Promise<PostResponse> {
152
+ const url = `${this.baseUrl}/teams/${encodeURIComponent(teamName)}/posts`;
153
+
154
+ logger.debug('Creating post', {
155
+ teamName,
156
+ url,
157
+ hasContent: !!postData.content,
158
+ tagsCount: postData.tags?.length || 0,
159
+ });
160
+
161
+ // Adapt to remote API schema - use 'author' instead of 'author_name'
162
+ const remotePostData = {
163
+ author: postData.author_name,
164
+ content: postData.content,
165
+ tags: postData.tags,
166
+ parentPostId: postData.parent_post_id,
167
+ };
168
+
169
+ const response = await this.makeRequest('POST', url, remotePostData);
170
+ const remoteResponse = response as RemotePostResponse;
171
+
172
+ // Adapt remote response back to our schema
173
+ const adaptedResponse: PostResponse = {
174
+ post: {
175
+ id: remoteResponse.postId,
176
+ author_name: remoteResponse.author,
177
+ content: remoteResponse.content,
178
+ tags: remoteResponse.tags || [],
179
+ timestamp: remoteResponse.createdAt?._seconds
180
+ ? new Date(remoteResponse.createdAt._seconds * 1000).toISOString()
181
+ : new Date().toISOString(),
182
+ parent_post_id: remoteResponse.parentPostId || undefined,
183
+ team_name: teamName,
184
+ },
185
+ };
186
+
187
+ return adaptedResponse;
188
+ }
189
+
190
+ /**
191
+ * Make an HTTP request with error handling and logging
192
+ */
193
+ private async makeRequest(method: string, url: string, body?: unknown): Promise<unknown> {
194
+ const controller = new AbortController();
195
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
196
+ const startTime = Date.now();
197
+
198
+ try {
199
+ const options: RequestInit = {
200
+ method,
201
+ headers: {
202
+ 'x-api-key': this.apiKey,
203
+ 'Content-Type': 'application/json',
204
+ Accept: 'application/json',
205
+ },
206
+ signal: controller.signal,
207
+ };
208
+
209
+ if (body) {
210
+ options.body = JSON.stringify(body);
211
+ }
212
+
213
+ logger.apiRequest(method, url, {
214
+ headers: {
215
+ 'Content-Type': 'application/json',
216
+ Accept: 'application/json',
217
+ },
218
+ hasBody: !!body,
219
+ timeout: this.timeout,
220
+ });
221
+
222
+ const response = await this.fetchFn(url, options);
223
+ const duration = Date.now() - startTime;
224
+
225
+ if (!response.ok) {
226
+ logger.apiResponse(method, url, response.status, duration, {
227
+ statusText: response.statusText,
228
+ failed: true,
229
+ });
230
+ throw await this.handleErrorResponse(response);
231
+ }
232
+
233
+ logger.apiResponse(method, url, response.status, duration);
234
+ const data = await response.json();
235
+ return data;
236
+ } catch (error) {
237
+ const duration = Date.now() - startTime;
238
+ if (error instanceof Error && error.name === 'AbortError') {
239
+ logger.apiError(method, url, new Error(`Request timeout after ${this.timeout}ms`), {
240
+ duration,
241
+ timeout: true,
242
+ });
243
+ throw new McpTimeoutError(`Request timeout after ${this.timeout}ms`, this.timeout);
244
+ }
245
+ logger.apiError(method, url, error instanceof Error ? error : new Error(String(error)), {
246
+ duration,
247
+ errorType: error instanceof Error ? error.name : 'unknown',
248
+ });
249
+ throw error;
250
+ } finally {
251
+ clearTimeout(timeoutId);
252
+ }
253
+ }
254
+
255
+ /**
256
+ * Handle error responses from the API
257
+ */
258
+ private async handleErrorResponse(response: Response): Promise<Error> {
259
+ let errorMessage = `API request failed: ${response.status} ${response.statusText}`;
260
+
261
+ try {
262
+ const errorData = (await response.json()) as {
263
+ error?: string;
264
+ message?: string;
265
+ code?: string;
266
+ };
267
+ errorMessage = errorData.error || errorData.message || errorMessage;
268
+ } catch {
269
+ // Ignore JSON parse errors
270
+ }
271
+
272
+ // Error handled, no logging needed for production
273
+
274
+ switch (response.status) {
275
+ case 401:
276
+ throw new McpAuthenticationError(`Authentication failed: ${errorMessage}`);
277
+ case 403:
278
+ throw new Error(`Access forbidden: ${errorMessage}`);
279
+ case 404:
280
+ throw new McpMethodNotFoundError(`Resource not found: ${errorMessage}`);
281
+ case 429:
282
+ throw new McpRateLimitError(`Rate limit exceeded: ${errorMessage}`);
283
+ case 500:
284
+ case 502:
285
+ case 503:
286
+ case 504:
287
+ throw new Error(`Server error: ${errorMessage}`);
288
+ default:
289
+ throw new Error(errorMessage);
290
+ }
291
+ }
292
+ }
package/src/config.ts ADDED
@@ -0,0 +1,92 @@
1
+ // ABOUTME: Configuration management for the MCP server
2
+ // ABOUTME: Loads and validates environment variables
3
+
4
+ import { readFileSync } from 'node:fs';
5
+ import { dirname, join } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { config as loadDotenv } from 'dotenv';
8
+ import type { ServerConfig } from './types.js';
9
+
10
+ loadDotenv();
11
+
12
+ // Centralized configuration keys - single source of truth
13
+ export const ENV_KEYS = {
14
+ SOCIALMEDIA_API_BASE_URL: 'SOCIALMEDIA_API_BASE_URL',
15
+ SOCIALMEDIA_API_KEY: 'SOCIALMEDIA_API_KEY',
16
+ SOCIALMEDIA_TEAM_ID: 'SOCIALMEDIA_TEAM_ID',
17
+ PORT: 'PORT',
18
+ LOG_LEVEL: 'LOG_LEVEL',
19
+ API_TIMEOUT: 'API_TIMEOUT',
20
+ MCP_TRANSPORT: 'MCP_TRANSPORT',
21
+ MCP_HTTP_PORT: 'MCP_HTTP_PORT',
22
+ MCP_HTTP_HOST: 'MCP_HTTP_HOST',
23
+ MCP_ENABLE_JSON: 'MCP_ENABLE_JSON',
24
+ MCP_CORS_ORIGIN: 'MCP_CORS_ORIGIN',
25
+ } as const;
26
+
27
+ // Get version from package.json
28
+ function getVersion(): string {
29
+ try {
30
+ const __filename = fileURLToPath(import.meta.url);
31
+ const __dirname = dirname(__filename);
32
+ const packageJsonPath = join(__dirname, '..', 'package.json');
33
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
34
+ return packageJson.version;
35
+ } catch (_error) {
36
+ // Fallback version if package.json can't be read
37
+ return '1.0.3';
38
+ }
39
+ }
40
+
41
+ export const version = getVersion();
42
+
43
+ function getEnvVar(name: string, defaultValue?: string): string {
44
+ const value = process.env[name];
45
+ if (!value && !defaultValue) {
46
+ throw new Error(`Missing required environment variable: ${name}`);
47
+ }
48
+ return value || defaultValue || '';
49
+ }
50
+
51
+ export function getConfig(): ServerConfig {
52
+ return {
53
+ socialApiBaseUrl: getEnvVar(ENV_KEYS.SOCIALMEDIA_API_BASE_URL),
54
+ socialApiKey: getEnvVar(ENV_KEYS.SOCIALMEDIA_API_KEY),
55
+ teamName: getEnvVar(ENV_KEYS.SOCIALMEDIA_TEAM_ID),
56
+ port: Number.parseInt(getEnvVar(ENV_KEYS.PORT, '3000'), 10),
57
+ logLevel: getEnvVar(ENV_KEYS.LOG_LEVEL, 'info'),
58
+ apiTimeout: Number.parseInt(getEnvVar(ENV_KEYS.API_TIMEOUT, '30000'), 10), // 30 seconds default
59
+ };
60
+ }
61
+
62
+ export const config: ServerConfig = getConfig();
63
+
64
+ export function validateConfig(): void {
65
+ const errors: string[] = [];
66
+
67
+ try {
68
+ const conf = getConfig();
69
+
70
+ if (!conf.socialApiBaseUrl) {
71
+ errors.push(`${ENV_KEYS.SOCIALMEDIA_API_BASE_URL} is required`);
72
+ }
73
+
74
+ if (!conf.socialApiKey) {
75
+ errors.push(`${ENV_KEYS.SOCIALMEDIA_API_KEY} is required`);
76
+ }
77
+
78
+ if (!conf.teamName) {
79
+ errors.push(`${ENV_KEYS.SOCIALMEDIA_TEAM_ID} is required`);
80
+ }
81
+
82
+ if (Number.isNaN(conf.port) || conf.port < 1 || conf.port > 65535) {
83
+ errors.push(`${ENV_KEYS.PORT} must be a valid port number (1-65535)`);
84
+ }
85
+ } catch (error) {
86
+ errors.push(error instanceof Error ? error.message : 'Unknown error');
87
+ }
88
+
89
+ if (errors.length > 0) {
90
+ throw new Error(`Configuration validation failed:\n${errors.join('\n')}`);
91
+ }
92
+ }