@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.
- package/LICENSE +21 -0
- package/README.md +154 -0
- package/bin/mcp-agent-social +30 -0
- package/dist/api-client.d.ts +31 -0
- package/dist/api-client.d.ts.map +1 -0
- package/dist/api-client.js +212 -0
- package/dist/api-client.js.map +1 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +79 -0
- package/dist/config.js.map +1 -0
- package/dist/hooks/index.d.ts +38 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +253 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/types.d.ts +35 -0
- package/dist/hooks/types.d.ts.map +1 -0
- package/dist/hooks/types.js +4 -0
- package/dist/hooks/types.js.map +1 -0
- package/dist/http-server.d.ts +38 -0
- package/dist/http-server.d.ts.map +1 -0
- package/dist/http-server.js +210 -0
- package/dist/http-server.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +186 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +44 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +281 -0
- package/dist/logger.js.map +1 -0
- package/dist/metrics.d.ts +47 -0
- package/dist/metrics.d.ts.map +1 -0
- package/dist/metrics.js +178 -0
- package/dist/metrics.js.map +1 -0
- package/dist/middleware/error-handler.d.ts +74 -0
- package/dist/middleware/error-handler.d.ts.map +1 -0
- package/dist/middleware/error-handler.js +218 -0
- package/dist/middleware/error-handler.js.map +1 -0
- package/dist/middleware/index.d.ts +55 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +91 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/middleware/timeout.d.ts +52 -0
- package/dist/middleware/timeout.d.ts.map +1 -0
- package/dist/middleware/timeout.js +189 -0
- package/dist/middleware/timeout.js.map +1 -0
- package/dist/middleware/validator.d.ts +25 -0
- package/dist/middleware/validator.d.ts.map +1 -0
- package/dist/middleware/validator.js +186 -0
- package/dist/middleware/validator.js.map +1 -0
- package/dist/prompts/analyze.d.ts +46 -0
- package/dist/prompts/analyze.d.ts.map +1 -0
- package/dist/prompts/analyze.js +351 -0
- package/dist/prompts/analyze.js.map +1 -0
- package/dist/prompts/generate.d.ts +48 -0
- package/dist/prompts/generate.d.ts.map +1 -0
- package/dist/prompts/generate.js +177 -0
- package/dist/prompts/generate.js.map +1 -0
- package/dist/prompts/index.d.ts +23 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +69 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/prompts/summarize.d.ts +32 -0
- package/dist/prompts/summarize.d.ts.map +1 -0
- package/dist/prompts/summarize.js +182 -0
- package/dist/prompts/summarize.js.map +1 -0
- package/dist/prompts/types.d.ts +34 -0
- package/dist/prompts/types.d.ts.map +1 -0
- package/dist/prompts/types.js +24 -0
- package/dist/prompts/types.js.map +1 -0
- package/dist/resources/agents.d.ts +17 -0
- package/dist/resources/agents.d.ts.map +1 -0
- package/dist/resources/agents.js +139 -0
- package/dist/resources/agents.js.map +1 -0
- package/dist/resources/feed.d.ts +19 -0
- package/dist/resources/feed.d.ts.map +1 -0
- package/dist/resources/feed.js +138 -0
- package/dist/resources/feed.js.map +1 -0
- package/dist/resources/index.d.ts +19 -0
- package/dist/resources/index.d.ts.map +1 -0
- package/dist/resources/index.js +146 -0
- package/dist/resources/index.js.map +1 -0
- package/dist/resources/posts.d.ts +17 -0
- package/dist/resources/posts.d.ts.map +1 -0
- package/dist/resources/posts.js +151 -0
- package/dist/resources/posts.js.map +1 -0
- package/dist/resources/types.d.ts +91 -0
- package/dist/resources/types.d.ts.map +1 -0
- package/dist/resources/types.js +12 -0
- package/dist/resources/types.js.map +1 -0
- package/dist/roots/index.d.ts +43 -0
- package/dist/roots/index.d.ts.map +1 -0
- package/dist/roots/index.js +131 -0
- package/dist/roots/index.js.map +1 -0
- package/dist/roots/types.d.ts +31 -0
- package/dist/roots/types.d.ts.map +1 -0
- package/dist/roots/types.js +4 -0
- package/dist/roots/types.js.map +1 -0
- package/dist/session-manager.d.ts +50 -0
- package/dist/session-manager.d.ts.map +1 -0
- package/dist/session-manager.js +127 -0
- package/dist/session-manager.js.map +1 -0
- package/dist/tools/create-post.d.ts +45 -0
- package/dist/tools/create-post.d.ts.map +1 -0
- package/dist/tools/create-post.js +119 -0
- package/dist/tools/create-post.js.map +1 -0
- package/dist/tools/index.d.ts +13 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +44 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/login.d.ts +35 -0
- package/dist/tools/login.d.ts.map +1 -0
- package/dist/tools/login.js +132 -0
- package/dist/tools/login.js.map +1 -0
- package/dist/tools/read-posts.d.ts +48 -0
- package/dist/tools/read-posts.d.ts.map +1 -0
- package/dist/tools/read-posts.js +93 -0
- package/dist/tools/read-posts.js.map +1 -0
- package/dist/types.d.ts +88 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/json.d.ts +13 -0
- package/dist/utils/json.d.ts.map +1 -0
- package/dist/utils/json.js +48 -0
- package/dist/utils/json.js.map +1 -0
- package/dist/validation.d.ts +58 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +223 -0
- package/dist/validation.js.map +1 -0
- package/package.json +70 -0
- package/src/api-client.ts +292 -0
- package/src/config.ts +92 -0
- package/src/hooks/index.ts +304 -0
- package/src/hooks/types.ts +44 -0
- package/src/http-server.ts +243 -0
- package/src/index.ts +213 -0
- package/src/logger.ts +326 -0
- package/src/metrics.ts +235 -0
- package/src/middleware/error-handler.ts +252 -0
- package/src/middleware/index.ts +112 -0
- package/src/middleware/timeout.ts +216 -0
- package/src/middleware/validator.ts +216 -0
- package/src/prompts/analyze.ts +404 -0
- package/src/prompts/generate.ts +217 -0
- package/src/prompts/index.ts +121 -0
- package/src/prompts/summarize.ts +217 -0
- package/src/prompts/types.ts +44 -0
- package/src/resources/agents.ts +165 -0
- package/src/resources/feed.ts +169 -0
- package/src/resources/index.ts +210 -0
- package/src/resources/posts.ts +179 -0
- package/src/resources/types.ts +104 -0
- package/src/roots/index.ts +166 -0
- package/src/roots/types.ts +36 -0
- package/src/session-manager.ts +149 -0
- package/src/tools/create-post.ts +154 -0
- package/src/tools/index.ts +70 -0
- package/src/tools/login.ts +169 -0
- package/src/tools/read-posts.ts +120 -0
- package/src/types.ts +107 -0
- package/src/utils/json.ts +46 -0
- package/src/validation.ts +322 -0
- 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
|
+
}
|