@elliotding/ai-agent-mcp 0.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 (191) hide show
  1. package/dist/api/cached-client.d.ts +48 -0
  2. package/dist/api/cached-client.d.ts.map +1 -0
  3. package/dist/api/cached-client.js +126 -0
  4. package/dist/api/cached-client.js.map +1 -0
  5. package/dist/api/client.d.ts +213 -0
  6. package/dist/api/client.d.ts.map +1 -0
  7. package/dist/api/client.js +326 -0
  8. package/dist/api/client.js.map +1 -0
  9. package/dist/auth/index.d.ts +8 -0
  10. package/dist/auth/index.d.ts.map +1 -0
  11. package/dist/auth/index.js +26 -0
  12. package/dist/auth/index.js.map +1 -0
  13. package/dist/auth/middleware.d.ts +36 -0
  14. package/dist/auth/middleware.d.ts.map +1 -0
  15. package/dist/auth/middleware.js +194 -0
  16. package/dist/auth/middleware.js.map +1 -0
  17. package/dist/auth/permissions.d.ts +60 -0
  18. package/dist/auth/permissions.d.ts.map +1 -0
  19. package/dist/auth/permissions.js +256 -0
  20. package/dist/auth/permissions.js.map +1 -0
  21. package/dist/auth/token-validator.d.ts +52 -0
  22. package/dist/auth/token-validator.d.ts.map +1 -0
  23. package/dist/auth/token-validator.js +217 -0
  24. package/dist/auth/token-validator.js.map +1 -0
  25. package/dist/cache/cache-manager.d.ts +49 -0
  26. package/dist/cache/cache-manager.d.ts.map +1 -0
  27. package/dist/cache/cache-manager.js +191 -0
  28. package/dist/cache/cache-manager.js.map +1 -0
  29. package/dist/cache/index.d.ts +6 -0
  30. package/dist/cache/index.d.ts.map +1 -0
  31. package/dist/cache/index.js +12 -0
  32. package/dist/cache/index.js.map +1 -0
  33. package/dist/cache/redis-client.d.ts +45 -0
  34. package/dist/cache/redis-client.d.ts.map +1 -0
  35. package/dist/cache/redis-client.js +210 -0
  36. package/dist/cache/redis-client.js.map +1 -0
  37. package/dist/config/constants.d.ts +28 -0
  38. package/dist/config/constants.d.ts.map +1 -0
  39. package/dist/config/constants.js +31 -0
  40. package/dist/config/constants.js.map +1 -0
  41. package/dist/config/index.d.ts +54 -0
  42. package/dist/config/index.d.ts.map +1 -0
  43. package/dist/config/index.js +168 -0
  44. package/dist/config/index.js.map +1 -0
  45. package/dist/filesystem/manager.d.ts +45 -0
  46. package/dist/filesystem/manager.d.ts.map +1 -0
  47. package/dist/filesystem/manager.js +246 -0
  48. package/dist/filesystem/manager.js.map +1 -0
  49. package/dist/git/multi-source-manager.d.ts +62 -0
  50. package/dist/git/multi-source-manager.d.ts.map +1 -0
  51. package/dist/git/multi-source-manager.js +293 -0
  52. package/dist/git/multi-source-manager.js.map +1 -0
  53. package/dist/git/operations.d.ts +27 -0
  54. package/dist/git/operations.d.ts.map +1 -0
  55. package/dist/git/operations.js +83 -0
  56. package/dist/git/operations.js.map +1 -0
  57. package/dist/index.d.ts +6 -0
  58. package/dist/index.d.ts.map +1 -0
  59. package/dist/index.js +109 -0
  60. package/dist/index.js.map +1 -0
  61. package/dist/monitoring/health.d.ts +35 -0
  62. package/dist/monitoring/health.d.ts.map +1 -0
  63. package/dist/monitoring/health.js +105 -0
  64. package/dist/monitoring/health.js.map +1 -0
  65. package/dist/resources/index.d.ts +6 -0
  66. package/dist/resources/index.d.ts.map +1 -0
  67. package/dist/resources/index.js +10 -0
  68. package/dist/resources/index.js.map +1 -0
  69. package/dist/resources/loader.d.ts +87 -0
  70. package/dist/resources/loader.d.ts.map +1 -0
  71. package/dist/resources/loader.js +452 -0
  72. package/dist/resources/loader.js.map +1 -0
  73. package/dist/server/http.d.ts +57 -0
  74. package/dist/server/http.d.ts.map +1 -0
  75. package/dist/server/http.js +336 -0
  76. package/dist/server/http.js.map +1 -0
  77. package/dist/server.d.ts +13 -0
  78. package/dist/server.d.ts.map +1 -0
  79. package/dist/server.js +157 -0
  80. package/dist/server.js.map +1 -0
  81. package/dist/session/manager.d.ts +91 -0
  82. package/dist/session/manager.d.ts.map +1 -0
  83. package/dist/session/manager.js +251 -0
  84. package/dist/session/manager.js.map +1 -0
  85. package/dist/tools/index.d.ts +11 -0
  86. package/dist/tools/index.d.ts.map +1 -0
  87. package/dist/tools/index.js +27 -0
  88. package/dist/tools/index.js.map +1 -0
  89. package/dist/tools/manage-subscription.d.ts +43 -0
  90. package/dist/tools/manage-subscription.d.ts.map +1 -0
  91. package/dist/tools/manage-subscription.js +268 -0
  92. package/dist/tools/manage-subscription.js.map +1 -0
  93. package/dist/tools/registry.d.ts +40 -0
  94. package/dist/tools/registry.d.ts.map +1 -0
  95. package/dist/tools/registry.js +85 -0
  96. package/dist/tools/registry.js.map +1 -0
  97. package/dist/tools/search-resources.d.ts +31 -0
  98. package/dist/tools/search-resources.d.ts.map +1 -0
  99. package/dist/tools/search-resources.js +154 -0
  100. package/dist/tools/search-resources.js.map +1 -0
  101. package/dist/tools/sync-resources.d.ts +41 -0
  102. package/dist/tools/sync-resources.d.ts.map +1 -0
  103. package/dist/tools/sync-resources.js +606 -0
  104. package/dist/tools/sync-resources.js.map +1 -0
  105. package/dist/tools/uninstall-resource.d.ts +30 -0
  106. package/dist/tools/uninstall-resource.d.ts.map +1 -0
  107. package/dist/tools/uninstall-resource.js +259 -0
  108. package/dist/tools/uninstall-resource.js.map +1 -0
  109. package/dist/tools/upload-resource.d.ts +77 -0
  110. package/dist/tools/upload-resource.d.ts.map +1 -0
  111. package/dist/tools/upload-resource.js +252 -0
  112. package/dist/tools/upload-resource.js.map +1 -0
  113. package/dist/transport/sse.d.ts +29 -0
  114. package/dist/transport/sse.d.ts.map +1 -0
  115. package/dist/transport/sse.js +271 -0
  116. package/dist/transport/sse.js.map +1 -0
  117. package/dist/types/errors.d.ts +60 -0
  118. package/dist/types/errors.d.ts.map +1 -0
  119. package/dist/types/errors.js +112 -0
  120. package/dist/types/errors.js.map +1 -0
  121. package/dist/types/index.d.ts +7 -0
  122. package/dist/types/index.d.ts.map +1 -0
  123. package/dist/types/index.js +23 -0
  124. package/dist/types/index.js.map +1 -0
  125. package/dist/types/mcp.d.ts +50 -0
  126. package/dist/types/mcp.d.ts.map +1 -0
  127. package/dist/types/mcp.js +6 -0
  128. package/dist/types/mcp.js.map +1 -0
  129. package/dist/types/resources.d.ts +109 -0
  130. package/dist/types/resources.d.ts.map +1 -0
  131. package/dist/types/resources.js +7 -0
  132. package/dist/types/resources.js.map +1 -0
  133. package/dist/types/tools.d.ts +147 -0
  134. package/dist/types/tools.d.ts.map +1 -0
  135. package/dist/types/tools.js +6 -0
  136. package/dist/types/tools.js.map +1 -0
  137. package/dist/utils/cursor-paths.d.ts +49 -0
  138. package/dist/utils/cursor-paths.d.ts.map +1 -0
  139. package/dist/utils/cursor-paths.js +116 -0
  140. package/dist/utils/cursor-paths.js.map +1 -0
  141. package/dist/utils/log-cleaner.d.ts +18 -0
  142. package/dist/utils/log-cleaner.d.ts.map +1 -0
  143. package/dist/utils/log-cleaner.js +112 -0
  144. package/dist/utils/log-cleaner.js.map +1 -0
  145. package/dist/utils/logger.d.ts +59 -0
  146. package/dist/utils/logger.d.ts.map +1 -0
  147. package/dist/utils/logger.js +292 -0
  148. package/dist/utils/logger.js.map +1 -0
  149. package/dist/utils/validation.d.ts +58 -0
  150. package/dist/utils/validation.d.ts.map +1 -0
  151. package/dist/utils/validation.js +214 -0
  152. package/dist/utils/validation.js.map +1 -0
  153. package/package.json +58 -0
  154. package/src/api/cached-client.ts +144 -0
  155. package/src/api/client.ts +578 -0
  156. package/src/auth/index.ts +11 -0
  157. package/src/auth/middleware.ts +244 -0
  158. package/src/auth/permissions.ts +317 -0
  159. package/src/auth/token-validator.ts +294 -0
  160. package/src/cache/cache-manager.ts +243 -0
  161. package/src/cache/index.ts +6 -0
  162. package/src/cache/redis-client.ts +249 -0
  163. package/src/config/constants.ts +33 -0
  164. package/src/config/index.ts +228 -0
  165. package/src/filesystem/manager.ts +235 -0
  166. package/src/git/multi-source-manager.ts +333 -0
  167. package/src/git/operations.ts +93 -0
  168. package/src/index.ts +139 -0
  169. package/src/monitoring/health.ts +132 -0
  170. package/src/resources/index.ts +13 -0
  171. package/src/resources/loader.ts +530 -0
  172. package/src/server/http.ts +427 -0
  173. package/src/server.ts +191 -0
  174. package/src/session/manager.ts +296 -0
  175. package/src/tools/index.ts +11 -0
  176. package/src/tools/manage-subscription.ts +332 -0
  177. package/src/tools/registry.ts +97 -0
  178. package/src/tools/search-resources.ts +177 -0
  179. package/src/tools/sync-resources.ts +662 -0
  180. package/src/tools/uninstall-resource.ts +248 -0
  181. package/src/tools/upload-resource.ts +258 -0
  182. package/src/transport/sse.ts +308 -0
  183. package/src/types/errors.ts +146 -0
  184. package/src/types/index.ts +7 -0
  185. package/src/types/mcp.ts +61 -0
  186. package/src/types/resources.ts +141 -0
  187. package/src/types/tools.ts +175 -0
  188. package/src/utils/cursor-paths.ts +83 -0
  189. package/src/utils/log-cleaner.ts +92 -0
  190. package/src/utils/logger.ts +333 -0
  191. package/src/utils/validation.ts +262 -0
@@ -0,0 +1,427 @@
1
+ /**
2
+ * HTTP Server with SSE Support
3
+ * Uses SDK SSEServerTransport for standard MCP-over-SSE protocol,
4
+ * matching the same pattern as the ACM MCP server.
5
+ */
6
+
7
+ import Fastify, { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
8
+ import cors from '@fastify/cors';
9
+ import helmet from '@fastify/helmet';
10
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
11
+ import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
12
+ import {
13
+ CallToolRequestSchema,
14
+ ListToolsRequestSchema,
15
+ } from '@modelcontextprotocol/sdk/types.js';
16
+ import { syncResources } from '../tools/sync-resources.js';
17
+ import { config } from '../config';
18
+ import { logger } from '../utils/logger';
19
+ import { sessionManager } from '../session/manager';
20
+ import { toolRegistry } from '../tools/registry';
21
+ import {
22
+ tokenAuthOrLegacyMiddleware,
23
+ checkToolCallPermission,
24
+ type AuthenticatedRequest,
25
+ } from '../auth/middleware';
26
+ import { HealthChecker, type HealthStatus as MonitoringHealthStatus } from '../monitoring/health.js';
27
+ import { CacheManager } from '../cache/cache-manager.js';
28
+
29
+ export interface HealthStatus {
30
+ status: 'healthy' | 'unhealthy';
31
+ uptime: number;
32
+ memory: {
33
+ used: number;
34
+ total: number;
35
+ percentage: number;
36
+ };
37
+ sessions: {
38
+ active: number;
39
+ total: number;
40
+ };
41
+ services: MonitoringHealthStatus['services'];
42
+ details?: MonitoringHealthStatus['details'];
43
+ timestamp: string;
44
+ }
45
+
46
+ export class HTTPServer {
47
+ private fastify: FastifyInstance;
48
+ private startTime: number = Date.now();
49
+ private healthChecker: HealthChecker | null = null;
50
+
51
+ /** Active SDK SSE transports keyed by sessionId */
52
+ private sseTransports: Map<string, SSEServerTransport> = new Map();
53
+
54
+ constructor(cacheManager?: CacheManager) {
55
+ this.fastify = Fastify({
56
+ logger: false,
57
+ bodyLimit: 10 * 1024 * 1024, // 10MB
58
+ });
59
+
60
+ if (cacheManager) {
61
+ this.healthChecker = new HealthChecker(cacheManager);
62
+ }
63
+
64
+ this.setupMiddleware();
65
+ this.setupRoutes();
66
+ }
67
+
68
+ // ─────────────────────────────────────────────────────────────────────────
69
+ // Middleware
70
+ // ─────────────────────────────────────────────────────────────────────────
71
+
72
+ private setupMiddleware(): void {
73
+ this.fastify.register(cors, {
74
+ origin: true,
75
+ credentials: true,
76
+ });
77
+
78
+ this.fastify.register(helmet, {
79
+ contentSecurityPolicy: false, // Disable for SSE
80
+ });
81
+
82
+ this.fastify.addHook('onRequest', async (request) => {
83
+ logger.debug(
84
+ { method: request.method, url: request.url, ip: request.ip },
85
+ 'HTTP request received'
86
+ );
87
+ });
88
+
89
+ this.fastify.addHook('onResponse', async (request) => {
90
+ logger.debug(
91
+ {
92
+ method: request.method,
93
+ url: request.url,
94
+ statusCode: (request.raw as { statusCode?: number }).statusCode || 200,
95
+ },
96
+ 'HTTP response sent'
97
+ );
98
+ });
99
+ }
100
+
101
+ // ─────────────────────────────────────────────────────────────────────────
102
+ // Routes
103
+ // ─────────────────────────────────────────────────────────────────────────
104
+
105
+ private setupRoutes(): void {
106
+ // Health check
107
+ this.fastify.get('/health', this.handleHealth.bind(this));
108
+
109
+ // SSE connection — GET establishes the stream (SDK standard)
110
+ this.fastify.get('/sse', {
111
+ preHandler: tokenAuthOrLegacyMiddleware,
112
+ handler: this.handleSSEConnection.bind(this),
113
+ });
114
+
115
+ // Message endpoint — POST delivers JSON-RPC messages, sessionId in query
116
+ this.fastify.post('/message', this.handleMessage.bind(this));
117
+
118
+ // OAuth discovery — return 404 so Cursor skips OAuth handshake
119
+ this.fastify.get('/.well-known/oauth-authorization-server', async (_req, reply) => {
120
+ reply.code(404).send({ error: 'OAuth not supported' });
121
+ });
122
+
123
+ // Root info
124
+ this.fastify.get('/', async () => ({
125
+ server: 'CSP AI Agent MCP Server',
126
+ version: '1.0.0',
127
+ transport: 'sse',
128
+ endpoints: {
129
+ health: 'GET /health',
130
+ sse: 'GET /sse',
131
+ message: 'POST /message?sessionId=<id>',
132
+ },
133
+ }));
134
+ }
135
+
136
+ // ─────────────────────────────────────────────────────────────────────────
137
+ // MCP Server factory
138
+ // ─────────────────────────────────────────────────────────────────────────
139
+
140
+ /**
141
+ * Creates a new SDK Server instance and registers all tools from toolRegistry.
142
+ * A fresh instance is created per SSE connection so that each session is
143
+ * isolated (matching ACM's createMCPServer-per-connection pattern).
144
+ */
145
+ private createMcpServer(userId?: string, email?: string, groups?: string[]): Server {
146
+ const server = new Server(
147
+ { name: 'csp-ai-agent-mcp', version: '0.2.0' },
148
+ { capabilities: { tools: {} } }
149
+ );
150
+
151
+ // tools/list
152
+ server.setRequestHandler(ListToolsRequestSchema, () => ({
153
+ tools: toolRegistry.getMCPToolDefinitions(),
154
+ }));
155
+
156
+ // Auto-sync subscribed resources once the MCP handshake is fully complete.
157
+ // Runs in the background so it never blocks the connection setup.
158
+ server.oninitialized = () => {
159
+ logger.info({ userId }, 'MCP initialized — triggering background sync_resources');
160
+ syncResources({ mode: 'incremental', scope: 'global' }).then((result) => {
161
+ if (result.success) {
162
+ logger.info(
163
+ { userId, synced: result.data?.summary?.synced, cached: result.data?.summary?.cached },
164
+ 'Auto sync_resources on connect completed'
165
+ );
166
+ } else {
167
+ logger.warn({ userId, error: result.error }, 'Auto sync_resources on connect failed');
168
+ }
169
+ }).catch((err) => {
170
+ logger.error({ userId, err }, 'Auto sync_resources on connect threw an error');
171
+ });
172
+ };
173
+
174
+ // tools/call
175
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
176
+ const { name, arguments: args = {} } = request.params;
177
+
178
+ // Permission check when user context is available
179
+ if (userId && groups && groups.length > 0) {
180
+ const permCheck = checkToolCallPermission(name, { userId, email: email ?? '', groups });
181
+ if (!permCheck.allowed) {
182
+ return {
183
+ content: [{ type: 'text' as const, text: `Permission denied: ${permCheck.reason}` }],
184
+ isError: true,
185
+ };
186
+ }
187
+ }
188
+
189
+ try {
190
+ const result = await toolRegistry.callTool(name, args as Record<string, unknown>);
191
+ const text = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
192
+ return { content: [{ type: 'text' as const, text }] };
193
+ } catch (err) {
194
+ const msg = err instanceof Error ? err.message : String(err);
195
+ logger.error({ toolName: name, err }, 'Tool execution failed');
196
+ return {
197
+ content: [{ type: 'text' as const, text: `Error: ${msg}` }],
198
+ isError: true,
199
+ };
200
+ }
201
+ });
202
+
203
+ return server;
204
+ }
205
+
206
+ // ─────────────────────────────────────────────────────────────────────────
207
+ // Handlers
208
+ // ─────────────────────────────────────────────────────────────────────────
209
+
210
+ /**
211
+ * GET /sse — establish SSE stream using SDK SSEServerTransport.
212
+ * Auth is validated via preHandler; user context is forwarded to the MCP server.
213
+ */
214
+ private async handleSSEConnection(
215
+ request: AuthenticatedRequest,
216
+ reply: FastifyReply
217
+ ): Promise<void> {
218
+ logger.info({ ip: request.ip }, 'SSE connection request received');
219
+
220
+ const authHeader = request.headers.authorization;
221
+ const token = authHeader?.replace(/^Bearer\s+/i, '');
222
+ if (!token) {
223
+ reply.code(401).send({ error: 'Unauthorized', message: 'Bearer token required' });
224
+ return;
225
+ }
226
+
227
+ try {
228
+ // Keep our session manager in sync for health/monitoring endpoints
229
+ const sessionOptions = request.user
230
+ ? { userId: request.user.userId, email: request.user.email, groups: request.user.groups }
231
+ : undefined;
232
+ const session = await sessionManager.createSession(token, request.ip ?? '', sessionOptions);
233
+
234
+ logger.info(
235
+ { sessionId: session.id, userId: session.userId, email: session.email },
236
+ 'Session created'
237
+ );
238
+
239
+ // Heartbeat to keep proxies/load-balancers from dropping idle SSE streams
240
+ const heartbeat = setInterval(() => {
241
+ if (!reply.raw.destroyed) {
242
+ try {
243
+ reply.raw.write('event: heartbeat\ndata: {}\n\n');
244
+ } catch {
245
+ clearInterval(heartbeat);
246
+ }
247
+ } else {
248
+ clearInterval(heartbeat);
249
+ }
250
+ }, 30_000);
251
+
252
+ // Create SDK transport — it takes ownership of reply.raw
253
+ const transport = new SSEServerTransport('/message', reply.raw);
254
+ const sdkSessionId = transport.sessionId;
255
+ this.sseTransports.set(sdkSessionId, transport);
256
+
257
+ transport.onclose = () => {
258
+ logger.info({ sdkSessionId, sessionId: session.id }, 'SSE transport closed');
259
+ clearInterval(heartbeat);
260
+ this.sseTransports.delete(sdkSessionId);
261
+ sessionManager.closeSession(session.id);
262
+ };
263
+
264
+ transport.onerror = (err: Error) => {
265
+ logger.warn({ sdkSessionId, error: err.message }, 'SSE transport error');
266
+ };
267
+
268
+ // Create a per-connection MCP Server and connect it to the transport
269
+ const mcpServer = this.createMcpServer(
270
+ request.user?.userId,
271
+ request.user?.email,
272
+ request.user?.groups
273
+ );
274
+ await mcpServer.connect(transport);
275
+
276
+ logger.info({ sdkSessionId }, 'SSE stream established');
277
+
278
+ // Handle client disconnect
279
+ request.raw.on('close', () => {
280
+ clearInterval(heartbeat);
281
+ transport.close().catch(() => {/* already logged via onclose */});
282
+ });
283
+
284
+ } catch (error) {
285
+ logger.error({ error }, 'Failed to establish SSE connection');
286
+ if (!reply.raw.headersSent) {
287
+ reply.code(500).send({ error: 'Internal Server Error', message: 'Failed to establish connection' });
288
+ }
289
+ }
290
+ }
291
+
292
+ /**
293
+ * POST /message?sessionId=<id> — deliver JSON-RPC message to the correct transport.
294
+ * The SDK transport's handlePostMessage handles parsing and routing internally.
295
+ */
296
+ private async handleMessage(request: FastifyRequest, reply: FastifyReply): Promise<void> {
297
+ const sessionId = (request.query as Record<string, string>)['sessionId'];
298
+
299
+ if (!sessionId) {
300
+ reply.code(400).send({ error: 'Bad Request', message: 'Missing sessionId query parameter' });
301
+ return;
302
+ }
303
+
304
+ const transport = this.sseTransports.get(sessionId);
305
+ if (!transport) {
306
+ logger.warn({ sessionId }, 'No active transport found for sessionId');
307
+ reply.code(404).send({
308
+ error: 'Not Found',
309
+ message: 'Session not found or expired',
310
+ details: { sessionId, suggestion: 'Reconnect via GET /sse' },
311
+ });
312
+ return;
313
+ }
314
+
315
+ logger.debug({ sessionId }, 'Forwarding message to SDK transport');
316
+
317
+ try {
318
+ await transport.handlePostMessage(request.raw, reply.raw, request.body);
319
+ } catch (error) {
320
+ logger.error({ error, sessionId }, 'Failed to handle message');
321
+ if (!reply.raw.headersSent) {
322
+ reply.code(500).send({ error: 'Internal Server Error', message: 'Failed to process message' });
323
+ }
324
+ }
325
+ }
326
+
327
+ // ─────────────────────────────────────────────────────────────────────────
328
+ // Health check
329
+ // ─────────────────────────────────────────────────────────────────────────
330
+
331
+ private async handleHealth(): Promise<HealthStatus> {
332
+ const uptime = Math.floor((Date.now() - this.startTime) / 1000);
333
+ const memUsage = process.memoryUsage();
334
+
335
+ let servicesHealth: MonitoringHealthStatus | null = null;
336
+ if (this.healthChecker) {
337
+ try {
338
+ servicesHealth = await this.healthChecker.check();
339
+ } catch (error) {
340
+ logger.error({ error }, 'Health check failed');
341
+ }
342
+ }
343
+
344
+ const health: HealthStatus = {
345
+ status: servicesHealth?.status || 'healthy',
346
+ uptime,
347
+ memory: {
348
+ used: Math.round(memUsage.heapUsed / 1024 / 1024),
349
+ total: Math.round(memUsage.heapTotal / 1024 / 1024),
350
+ percentage: Math.round((memUsage.heapUsed / memUsage.heapTotal) * 100),
351
+ },
352
+ sessions: {
353
+ active: sessionManager.getActiveSessionCount(),
354
+ total: sessionManager.getTotalSessionCount(),
355
+ },
356
+ services: servicesHealth?.services || {
357
+ http: 'up',
358
+ redis: 'not_configured',
359
+ cache: 'down',
360
+ },
361
+ timestamp: new Date().toISOString(),
362
+ };
363
+
364
+ if (servicesHealth?.details) {
365
+ health.details = servicesHealth.details;
366
+ }
367
+
368
+ logger.info({ health }, 'Health check requested');
369
+ return health;
370
+ }
371
+
372
+ // ─────────────────────────────────────────────────────────────────────────
373
+ // Lifecycle
374
+ // ─────────────────────────────────────────────────────────────────────────
375
+
376
+ async start(): Promise<void> {
377
+ try {
378
+ const host = config.http?.host || '0.0.0.0';
379
+ const port = config.http?.port || 3000;
380
+
381
+ await this.fastify.listen({ host, port });
382
+
383
+ logger.info({ host, port }, 'HTTP server started');
384
+ logger.info(`Health check: http://${host}:${port}/health`);
385
+ logger.info(`SSE endpoint: http://${host}:${port}/sse`);
386
+ logger.info(`Message endpoint: http://${host}:${port}/message?sessionId=<id>`);
387
+ } catch (error) {
388
+ logger.error({ error }, 'Failed to start HTTP server');
389
+ throw error;
390
+ }
391
+ }
392
+
393
+ async stop(): Promise<void> {
394
+ try {
395
+ logger.info('Stopping HTTP server gracefully...');
396
+
397
+ // Close all SDK SSE transports
398
+ for (const [id, transport] of this.sseTransports.entries()) {
399
+ logger.info({ id }, 'Closing SDK SSE transport');
400
+ await transport.close().catch(() => {});
401
+ }
402
+ this.sseTransports.clear();
403
+
404
+ sessionManager.closeAllSessions();
405
+
406
+ await new Promise(resolve => setTimeout(resolve, 500));
407
+ await this.fastify.close();
408
+
409
+ logger.info('HTTP server stopped gracefully');
410
+ } catch (error) {
411
+ logger.error({ error }, 'Error stopping HTTP server');
412
+ throw error;
413
+ }
414
+ }
415
+
416
+ getFastify(): FastifyInstance {
417
+ return this.fastify;
418
+ }
419
+
420
+ setCacheManager(cacheManager: CacheManager): void {
421
+ this.healthChecker = new HealthChecker(cacheManager);
422
+ logger.info('Health checker initialized with cache manager');
423
+ }
424
+ }
425
+
426
+ // Singleton instance (initialized without cache manager initially)
427
+ export const httpServer = new HTTPServer();
package/src/server.ts ADDED
@@ -0,0 +1,191 @@
1
+ /**
2
+ * MCP Server Main Logic
3
+ * Implements Model Context Protocol server with dual transport support
4
+ */
5
+
6
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
7
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
8
+ import {
9
+ CallToolRequestSchema,
10
+ ListToolsRequestSchema,
11
+ } from '@modelcontextprotocol/sdk/types.js';
12
+ import { logger } from './utils/logger';
13
+ import { config } from './config';
14
+ import { toolRegistry } from './tools/registry';
15
+ import {
16
+ syncResourcesTool,
17
+ manageSubscriptionTool,
18
+ searchResourcesTool,
19
+ uploadResourceTool,
20
+ uninstallResourceTool,
21
+ } from './tools';
22
+ import { httpServer } from './server/http';
23
+
24
+ let server: Server | null = null;
25
+
26
+ /**
27
+ * Register all MCP tools
28
+ */
29
+ function registerTools() {
30
+ logger.info('Registering MCP tools...');
31
+
32
+ toolRegistry.registerTool(syncResourcesTool);
33
+ toolRegistry.registerTool(manageSubscriptionTool);
34
+ toolRegistry.registerTool(searchResourcesTool);
35
+ toolRegistry.registerTool(uploadResourceTool);
36
+ toolRegistry.registerTool(uninstallResourceTool);
37
+
38
+ logger.info(
39
+ { toolCount: toolRegistry.getToolCount() },
40
+ `Registered ${toolRegistry.getToolCount()} MCP tools`
41
+ );
42
+ }
43
+
44
+ /**
45
+ * Start MCP Server with stdio transport
46
+ */
47
+ async function startStdioServer(): Promise<void> {
48
+ logger.info('Starting MCP Server with stdio transport...');
49
+
50
+ // Create MCP Server
51
+ server = new Server(
52
+ {
53
+ name: 'csp-ai-agent-mcp',
54
+ version: '0.2.0',
55
+ },
56
+ {
57
+ capabilities: {
58
+ tools: {},
59
+ },
60
+ }
61
+ );
62
+
63
+ // Handle tools/list request
64
+ server.setRequestHandler(ListToolsRequestSchema, () => {
65
+ const tools = toolRegistry.getMCPToolDefinitions();
66
+ logger.debug({ toolCount: tools.length }, 'tools/list request handled');
67
+ return {
68
+ tools,
69
+ };
70
+ });
71
+
72
+ // Handle tools/call request
73
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
74
+ const { name, arguments: args } = request.params;
75
+
76
+ logger.info({ toolName: name, arguments: args }, `tools/call request: ${name}`);
77
+
78
+ const tool = toolRegistry.getTool(name);
79
+ if (!tool) {
80
+ const error = `Tool not found: ${name}`;
81
+ logger.error({ toolName: name }, error);
82
+ return {
83
+ content: [
84
+ {
85
+ type: 'text' as const,
86
+ text: JSON.stringify({
87
+ success: false,
88
+ error: {
89
+ code: 'TOOL_NOT_FOUND',
90
+ message: error,
91
+ },
92
+ }),
93
+ },
94
+ ],
95
+ isError: true,
96
+ };
97
+ }
98
+
99
+ try {
100
+ // Call the tool handler
101
+ const result = await tool.handler(args || {});
102
+
103
+ return {
104
+ content: [
105
+ {
106
+ type: 'text' as const,
107
+ text: JSON.stringify(result),
108
+ },
109
+ ],
110
+ };
111
+ } catch (error) {
112
+ const errorMessage = error instanceof Error ? error.message : String(error);
113
+ logger.error({ toolName: name, error: errorMessage }, `Tool execution failed: ${name}`);
114
+
115
+ return {
116
+ content: [
117
+ {
118
+ type: 'text' as const,
119
+ text: JSON.stringify({
120
+ success: false,
121
+ error: {
122
+ code: 'TOOL_EXECUTION_ERROR',
123
+ message: errorMessage,
124
+ },
125
+ }),
126
+ },
127
+ ],
128
+ isError: true,
129
+ };
130
+ }
131
+ });
132
+
133
+ // Connect to stdio transport
134
+ const transport = new StdioServerTransport();
135
+ await server.connect(transport);
136
+
137
+ logger.info('✅ MCP Server started successfully (stdio transport)');
138
+ }
139
+
140
+ /**
141
+ * Start MCP Server with SSE transport
142
+ */
143
+ async function startSSEServer(): Promise<void> {
144
+ logger.info('Starting MCP Server with SSE transport...');
145
+
146
+ // Start HTTP server
147
+ await httpServer.start();
148
+
149
+ logger.info('✅ MCP Server started successfully (SSE transport)');
150
+ }
151
+
152
+ /**
153
+ * Start MCP Server (auto-detect transport mode)
154
+ */
155
+ export async function startServer(): Promise<void> {
156
+ // Register all tools (common for both transports)
157
+ registerTools();
158
+
159
+ // Start server based on transport mode
160
+ const transportMode = config.transport.mode;
161
+
162
+ logger.info({ transportMode }, `Starting server with ${transportMode} transport`);
163
+
164
+ if (transportMode === 'sse') {
165
+ await startSSEServer();
166
+ } else {
167
+ await startStdioServer();
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Stop MCP Server
173
+ */
174
+ export async function stopServer(): Promise<void> {
175
+ logger.info('Stopping MCP Server...');
176
+
177
+ const transportMode = config.transport.mode;
178
+
179
+ if (transportMode === 'sse') {
180
+ // Stop HTTP server
181
+ await httpServer.stop();
182
+ } else {
183
+ // Stop stdio server
184
+ if (server) {
185
+ await server.close();
186
+ server = null;
187
+ }
188
+ }
189
+
190
+ logger.info('MCP Server stopped');
191
+ }