@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.
- package/dist/api/cached-client.d.ts +48 -0
- package/dist/api/cached-client.d.ts.map +1 -0
- package/dist/api/cached-client.js +126 -0
- package/dist/api/cached-client.js.map +1 -0
- package/dist/api/client.d.ts +213 -0
- package/dist/api/client.d.ts.map +1 -0
- package/dist/api/client.js +326 -0
- package/dist/api/client.js.map +1 -0
- package/dist/auth/index.d.ts +8 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +26 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/middleware.d.ts +36 -0
- package/dist/auth/middleware.d.ts.map +1 -0
- package/dist/auth/middleware.js +194 -0
- package/dist/auth/middleware.js.map +1 -0
- package/dist/auth/permissions.d.ts +60 -0
- package/dist/auth/permissions.d.ts.map +1 -0
- package/dist/auth/permissions.js +256 -0
- package/dist/auth/permissions.js.map +1 -0
- package/dist/auth/token-validator.d.ts +52 -0
- package/dist/auth/token-validator.d.ts.map +1 -0
- package/dist/auth/token-validator.js +217 -0
- package/dist/auth/token-validator.js.map +1 -0
- package/dist/cache/cache-manager.d.ts +49 -0
- package/dist/cache/cache-manager.d.ts.map +1 -0
- package/dist/cache/cache-manager.js +191 -0
- package/dist/cache/cache-manager.js.map +1 -0
- package/dist/cache/index.d.ts +6 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +12 -0
- package/dist/cache/index.js.map +1 -0
- package/dist/cache/redis-client.d.ts +45 -0
- package/dist/cache/redis-client.d.ts.map +1 -0
- package/dist/cache/redis-client.js +210 -0
- package/dist/cache/redis-client.js.map +1 -0
- package/dist/config/constants.d.ts +28 -0
- package/dist/config/constants.d.ts.map +1 -0
- package/dist/config/constants.js +31 -0
- package/dist/config/constants.js.map +1 -0
- package/dist/config/index.d.ts +54 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +168 -0
- package/dist/config/index.js.map +1 -0
- package/dist/filesystem/manager.d.ts +45 -0
- package/dist/filesystem/manager.d.ts.map +1 -0
- package/dist/filesystem/manager.js +246 -0
- package/dist/filesystem/manager.js.map +1 -0
- package/dist/git/multi-source-manager.d.ts +62 -0
- package/dist/git/multi-source-manager.d.ts.map +1 -0
- package/dist/git/multi-source-manager.js +293 -0
- package/dist/git/multi-source-manager.js.map +1 -0
- package/dist/git/operations.d.ts +27 -0
- package/dist/git/operations.d.ts.map +1 -0
- package/dist/git/operations.js +83 -0
- package/dist/git/operations.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +109 -0
- package/dist/index.js.map +1 -0
- package/dist/monitoring/health.d.ts +35 -0
- package/dist/monitoring/health.d.ts.map +1 -0
- package/dist/monitoring/health.js +105 -0
- package/dist/monitoring/health.js.map +1 -0
- package/dist/resources/index.d.ts +6 -0
- package/dist/resources/index.d.ts.map +1 -0
- package/dist/resources/index.js +10 -0
- package/dist/resources/index.js.map +1 -0
- package/dist/resources/loader.d.ts +87 -0
- package/dist/resources/loader.d.ts.map +1 -0
- package/dist/resources/loader.js +452 -0
- package/dist/resources/loader.js.map +1 -0
- package/dist/server/http.d.ts +57 -0
- package/dist/server/http.d.ts.map +1 -0
- package/dist/server/http.js +336 -0
- package/dist/server/http.js.map +1 -0
- package/dist/server.d.ts +13 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +157 -0
- package/dist/server.js.map +1 -0
- package/dist/session/manager.d.ts +91 -0
- package/dist/session/manager.d.ts.map +1 -0
- package/dist/session/manager.js +251 -0
- package/dist/session/manager.js.map +1 -0
- package/dist/tools/index.d.ts +11 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +27 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/manage-subscription.d.ts +43 -0
- package/dist/tools/manage-subscription.d.ts.map +1 -0
- package/dist/tools/manage-subscription.js +268 -0
- package/dist/tools/manage-subscription.js.map +1 -0
- package/dist/tools/registry.d.ts +40 -0
- package/dist/tools/registry.d.ts.map +1 -0
- package/dist/tools/registry.js +85 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/tools/search-resources.d.ts +31 -0
- package/dist/tools/search-resources.d.ts.map +1 -0
- package/dist/tools/search-resources.js +154 -0
- package/dist/tools/search-resources.js.map +1 -0
- package/dist/tools/sync-resources.d.ts +41 -0
- package/dist/tools/sync-resources.d.ts.map +1 -0
- package/dist/tools/sync-resources.js +606 -0
- package/dist/tools/sync-resources.js.map +1 -0
- package/dist/tools/uninstall-resource.d.ts +30 -0
- package/dist/tools/uninstall-resource.d.ts.map +1 -0
- package/dist/tools/uninstall-resource.js +259 -0
- package/dist/tools/uninstall-resource.js.map +1 -0
- package/dist/tools/upload-resource.d.ts +77 -0
- package/dist/tools/upload-resource.d.ts.map +1 -0
- package/dist/tools/upload-resource.js +252 -0
- package/dist/tools/upload-resource.js.map +1 -0
- package/dist/transport/sse.d.ts +29 -0
- package/dist/transport/sse.d.ts.map +1 -0
- package/dist/transport/sse.js +271 -0
- package/dist/transport/sse.js.map +1 -0
- package/dist/types/errors.d.ts +60 -0
- package/dist/types/errors.d.ts.map +1 -0
- package/dist/types/errors.js +112 -0
- package/dist/types/errors.js.map +1 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +23 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/mcp.d.ts +50 -0
- package/dist/types/mcp.d.ts.map +1 -0
- package/dist/types/mcp.js +6 -0
- package/dist/types/mcp.js.map +1 -0
- package/dist/types/resources.d.ts +109 -0
- package/dist/types/resources.d.ts.map +1 -0
- package/dist/types/resources.js +7 -0
- package/dist/types/resources.js.map +1 -0
- package/dist/types/tools.d.ts +147 -0
- package/dist/types/tools.d.ts.map +1 -0
- package/dist/types/tools.js +6 -0
- package/dist/types/tools.js.map +1 -0
- package/dist/utils/cursor-paths.d.ts +49 -0
- package/dist/utils/cursor-paths.d.ts.map +1 -0
- package/dist/utils/cursor-paths.js +116 -0
- package/dist/utils/cursor-paths.js.map +1 -0
- package/dist/utils/log-cleaner.d.ts +18 -0
- package/dist/utils/log-cleaner.d.ts.map +1 -0
- package/dist/utils/log-cleaner.js +112 -0
- package/dist/utils/log-cleaner.js.map +1 -0
- package/dist/utils/logger.d.ts +59 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +292 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/validation.d.ts +58 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +214 -0
- package/dist/utils/validation.js.map +1 -0
- package/package.json +58 -0
- package/src/api/cached-client.ts +144 -0
- package/src/api/client.ts +578 -0
- package/src/auth/index.ts +11 -0
- package/src/auth/middleware.ts +244 -0
- package/src/auth/permissions.ts +317 -0
- package/src/auth/token-validator.ts +294 -0
- package/src/cache/cache-manager.ts +243 -0
- package/src/cache/index.ts +6 -0
- package/src/cache/redis-client.ts +249 -0
- package/src/config/constants.ts +33 -0
- package/src/config/index.ts +228 -0
- package/src/filesystem/manager.ts +235 -0
- package/src/git/multi-source-manager.ts +333 -0
- package/src/git/operations.ts +93 -0
- package/src/index.ts +139 -0
- package/src/monitoring/health.ts +132 -0
- package/src/resources/index.ts +13 -0
- package/src/resources/loader.ts +530 -0
- package/src/server/http.ts +427 -0
- package/src/server.ts +191 -0
- package/src/session/manager.ts +296 -0
- package/src/tools/index.ts +11 -0
- package/src/tools/manage-subscription.ts +332 -0
- package/src/tools/registry.ts +97 -0
- package/src/tools/search-resources.ts +177 -0
- package/src/tools/sync-resources.ts +662 -0
- package/src/tools/uninstall-resource.ts +248 -0
- package/src/tools/upload-resource.ts +258 -0
- package/src/transport/sse.ts +308 -0
- package/src/types/errors.ts +146 -0
- package/src/types/index.ts +7 -0
- package/src/types/mcp.ts +61 -0
- package/src/types/resources.ts +141 -0
- package/src/types/tools.ts +175 -0
- package/src/utils/cursor-paths.ts +83 -0
- package/src/utils/log-cleaner.ts +92 -0
- package/src/utils/logger.ts +333 -0
- package/src/utils/validation.ts +262 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Manager
|
|
3
|
+
* Manages SSE sessions and connections
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { randomUUID } from 'crypto';
|
|
7
|
+
import type { ServerResponse } from 'http';
|
|
8
|
+
import { logger } from '../utils/logger';
|
|
9
|
+
import { config } from '../config';
|
|
10
|
+
|
|
11
|
+
export interface Session {
|
|
12
|
+
id: string;
|
|
13
|
+
userId: string;
|
|
14
|
+
email: string;
|
|
15
|
+
groups: string[]; // Changed from 'roles' to 'groups'
|
|
16
|
+
token: string;
|
|
17
|
+
ip: string;
|
|
18
|
+
createdAt: Date;
|
|
19
|
+
lastActivity: Date;
|
|
20
|
+
connection?: ServerResponse;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface CreateSessionOptions {
|
|
24
|
+
userId?: string;
|
|
25
|
+
email?: string;
|
|
26
|
+
groups?: string[]; // Changed from 'roles' to 'groups'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class SessionManager {
|
|
30
|
+
private sessions: Map<string, Session> = new Map();
|
|
31
|
+
private totalSessions: number = 0;
|
|
32
|
+
private cleanupInterval?: NodeJS.Timeout;
|
|
33
|
+
|
|
34
|
+
constructor() {
|
|
35
|
+
// Start cleanup interval
|
|
36
|
+
this.startCleanup();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Create new session
|
|
41
|
+
* @param token - Auth token (JWT from CSP)
|
|
42
|
+
* @param ip - Client IP address
|
|
43
|
+
* @param options - User info from /user/permissions API (userId, email, groups)
|
|
44
|
+
*/
|
|
45
|
+
async createSession(
|
|
46
|
+
token: string,
|
|
47
|
+
ip: string,
|
|
48
|
+
options?: CreateSessionOptions
|
|
49
|
+
): Promise<Session> {
|
|
50
|
+
const sessionId = randomUUID();
|
|
51
|
+
|
|
52
|
+
const userId = options?.userId ?? this.extractUserIdFromToken(token);
|
|
53
|
+
const email = options?.email ?? '';
|
|
54
|
+
const groups = options?.groups ?? [];
|
|
55
|
+
|
|
56
|
+
const session: Session = {
|
|
57
|
+
id: sessionId,
|
|
58
|
+
userId,
|
|
59
|
+
email,
|
|
60
|
+
groups,
|
|
61
|
+
token,
|
|
62
|
+
ip,
|
|
63
|
+
createdAt: new Date(),
|
|
64
|
+
lastActivity: new Date(),
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
this.sessions.set(sessionId, session);
|
|
68
|
+
this.totalSessions++;
|
|
69
|
+
|
|
70
|
+
logger.info({ sessionId, userId, email, groups, ip }, 'Session created');
|
|
71
|
+
|
|
72
|
+
return session;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get session by ID
|
|
77
|
+
*/
|
|
78
|
+
getSession(sessionId: string): Session | undefined {
|
|
79
|
+
return this.sessions.get(sessionId);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Register SSE connection with session
|
|
84
|
+
*/
|
|
85
|
+
registerConnection(sessionId: string, connection: ServerResponse): void {
|
|
86
|
+
const session = this.sessions.get(sessionId);
|
|
87
|
+
if (session) {
|
|
88
|
+
session.connection = connection;
|
|
89
|
+
logger.debug({ sessionId }, 'Connection registered with session');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Send message to session via SSE
|
|
95
|
+
* Enhanced error handling to prevent EPIPE errors
|
|
96
|
+
* Note: This is synchronous for SSE transport compatibility
|
|
97
|
+
*/
|
|
98
|
+
sendMessage(sessionId: string, message: unknown): boolean {
|
|
99
|
+
const session = this.sessions.get(sessionId);
|
|
100
|
+
if (!session || !session.connection || session.connection.destroyed) {
|
|
101
|
+
logger.debug({ sessionId }, 'Cannot send message: session not found or connection closed');
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const data = JSON.stringify(message);
|
|
107
|
+
|
|
108
|
+
// Use write callback to detect errors (fire-and-forget)
|
|
109
|
+
let writeSuccess = true;
|
|
110
|
+
session.connection.write(`data: ${data}\n\n`, (err: Error | null | undefined) => {
|
|
111
|
+
if (err) {
|
|
112
|
+
// Handle specific error types
|
|
113
|
+
if (err.message.includes('EPIPE') || err.message.includes('ECONNRESET')) {
|
|
114
|
+
logger.debug({
|
|
115
|
+
sessionId,
|
|
116
|
+
error: err.message
|
|
117
|
+
}, 'Message write failed (client disconnected)');
|
|
118
|
+
} else {
|
|
119
|
+
logger.warn({
|
|
120
|
+
sessionId,
|
|
121
|
+
error: err.message
|
|
122
|
+
}, 'Message write failed');
|
|
123
|
+
}
|
|
124
|
+
writeSuccess = false;
|
|
125
|
+
} else {
|
|
126
|
+
logger.debug({ sessionId, messageSize: data.length }, 'Message sent to session');
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
return writeSuccess;
|
|
131
|
+
} catch (error) {
|
|
132
|
+
// Catch synchronous errors
|
|
133
|
+
if (error instanceof Error) {
|
|
134
|
+
if (error.message.includes('EPIPE') || error.message.includes('ECONNRESET')) {
|
|
135
|
+
logger.debug({
|
|
136
|
+
sessionId,
|
|
137
|
+
error: error.message
|
|
138
|
+
}, 'Message send failed (client disconnected)');
|
|
139
|
+
} else {
|
|
140
|
+
logger.error({
|
|
141
|
+
error: error.message,
|
|
142
|
+
sessionId
|
|
143
|
+
}, 'Failed to send message to session');
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Update session activity timestamp
|
|
152
|
+
*/
|
|
153
|
+
updateActivity(sessionId: string): void {
|
|
154
|
+
const session = this.sessions.get(sessionId);
|
|
155
|
+
if (session) {
|
|
156
|
+
session.lastActivity = new Date();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Close session with improved error handling
|
|
162
|
+
*/
|
|
163
|
+
closeSession(sessionId: string): void {
|
|
164
|
+
const session = this.sessions.get(sessionId);
|
|
165
|
+
if (session) {
|
|
166
|
+
// Close connection if exists
|
|
167
|
+
if (session.connection && !session.connection.destroyed) {
|
|
168
|
+
try {
|
|
169
|
+
// Try to send close message
|
|
170
|
+
session.connection.write(`data: ${JSON.stringify({ type: 'close' })}\n\n`, (err) => {
|
|
171
|
+
if (err) {
|
|
172
|
+
logger.debug({
|
|
173
|
+
sessionId,
|
|
174
|
+
error: err.message
|
|
175
|
+
}, 'Close message write failed (expected if client already disconnected)');
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// End connection
|
|
180
|
+
session.connection.end();
|
|
181
|
+
} catch (error) {
|
|
182
|
+
if (error instanceof Error) {
|
|
183
|
+
// Only log non-EPIPE errors as warnings
|
|
184
|
+
if (error.message.includes('EPIPE') || error.message.includes('ECONNRESET')) {
|
|
185
|
+
logger.debug({
|
|
186
|
+
sessionId,
|
|
187
|
+
error: error.message
|
|
188
|
+
}, 'Connection already closed');
|
|
189
|
+
} else {
|
|
190
|
+
logger.warn({
|
|
191
|
+
sessionId,
|
|
192
|
+
error: error.message
|
|
193
|
+
}, 'Error closing connection');
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Remove session
|
|
200
|
+
this.sessions.delete(sessionId);
|
|
201
|
+
logger.info({ sessionId }, 'Session closed');
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Close all sessions
|
|
207
|
+
*/
|
|
208
|
+
closeAllSessions(): void {
|
|
209
|
+
logger.info({ count: this.sessions.size }, 'Closing all sessions');
|
|
210
|
+
|
|
211
|
+
for (const [sessionId] of this.sessions) {
|
|
212
|
+
this.closeSession(sessionId);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Get active session count
|
|
218
|
+
*/
|
|
219
|
+
getActiveSessionCount(): number {
|
|
220
|
+
return this.sessions.size;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Get total session count (since server start)
|
|
225
|
+
*/
|
|
226
|
+
getTotalSessionCount(): number {
|
|
227
|
+
return this.totalSessions;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Get all active sessions
|
|
232
|
+
*/
|
|
233
|
+
getActiveSessions(): Session[] {
|
|
234
|
+
return Array.from(this.sessions.values());
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Start cleanup interval for expired sessions
|
|
239
|
+
*/
|
|
240
|
+
private startCleanup(): void {
|
|
241
|
+
const timeout = config.session?.timeout || 3600; // Default 1 hour
|
|
242
|
+
const cleanupInterval = 60000; // Check every 1 minute
|
|
243
|
+
|
|
244
|
+
this.cleanupInterval = setInterval(() => {
|
|
245
|
+
this.cleanupExpiredSessions(timeout);
|
|
246
|
+
}, cleanupInterval);
|
|
247
|
+
|
|
248
|
+
logger.info({ timeout, cleanupInterval }, 'Session cleanup started');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Stop cleanup interval
|
|
253
|
+
*/
|
|
254
|
+
stopCleanup(): void {
|
|
255
|
+
if (this.cleanupInterval) {
|
|
256
|
+
clearInterval(this.cleanupInterval);
|
|
257
|
+
this.cleanupInterval = undefined;
|
|
258
|
+
logger.info('Session cleanup stopped');
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Cleanup expired sessions
|
|
264
|
+
*/
|
|
265
|
+
private cleanupExpiredSessions(timeoutSeconds: number): void {
|
|
266
|
+
const now = Date.now();
|
|
267
|
+
const expiredSessions: string[] = [];
|
|
268
|
+
|
|
269
|
+
for (const [sessionId, session] of this.sessions) {
|
|
270
|
+
const inactiveTime = (now - session.lastActivity.getTime()) / 1000;
|
|
271
|
+
if (inactiveTime > timeoutSeconds) {
|
|
272
|
+
expiredSessions.push(sessionId);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (expiredSessions.length > 0) {
|
|
277
|
+
logger.info({ count: expiredSessions.length }, 'Cleaning up expired sessions');
|
|
278
|
+
for (const sessionId of expiredSessions) {
|
|
279
|
+
this.closeSession(sessionId);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Extract user ID from token (simplified)
|
|
286
|
+
* TODO: Implement proper JWT validation in Stage 5
|
|
287
|
+
*/
|
|
288
|
+
private extractUserIdFromToken(token: string): string {
|
|
289
|
+
// For now, use a simple hash or the token itself
|
|
290
|
+
// In Stage 5, this should decode and validate JWT
|
|
291
|
+
return token.substring(0, 16);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Singleton instance
|
|
296
|
+
export const sessionManager = new SessionManager();
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tools Index
|
|
3
|
+
* Exports all MCP tools
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export * from './sync-resources';
|
|
7
|
+
export * from './manage-subscription';
|
|
8
|
+
export * from './search-resources';
|
|
9
|
+
export * from './upload-resource';
|
|
10
|
+
export * from './uninstall-resource';
|
|
11
|
+
export * from './registry';
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* manage_subscription Tool
|
|
3
|
+
* Manage resource subscriptions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { logger, logToolCall } from '../utils/logger';
|
|
7
|
+
import { apiClient } from '../api/client';
|
|
8
|
+
import { MCPServerError, createValidationError } from '../types/errors';
|
|
9
|
+
import type { ManageSubscriptionParams, ManageSubscriptionResult, ToolResult } from '../types/tools';
|
|
10
|
+
import { syncResources } from './sync-resources';
|
|
11
|
+
import { uninstallResource } from './uninstall-resource';
|
|
12
|
+
|
|
13
|
+
export async function manageSubscription(params: unknown): Promise<ToolResult<ManageSubscriptionResult>> {
|
|
14
|
+
const startTime = Date.now();
|
|
15
|
+
|
|
16
|
+
// Type assertion for params
|
|
17
|
+
const typedParams = params as ManageSubscriptionParams;
|
|
18
|
+
|
|
19
|
+
logger.info({ tool: 'manage_subscription', params }, 'manage_subscription called');
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
let result: ManageSubscriptionResult;
|
|
23
|
+
|
|
24
|
+
switch (typedParams.action) {
|
|
25
|
+
case 'subscribe': {
|
|
26
|
+
// Validate resource_ids
|
|
27
|
+
if (!typedParams.resource_ids || typedParams.resource_ids.length === 0) {
|
|
28
|
+
throw createValidationError(
|
|
29
|
+
'resource_ids',
|
|
30
|
+
'array',
|
|
31
|
+
'resource_ids is required for subscribe action'
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
logger.debug({ resourceIds: typedParams.resource_ids, autoSync: typedParams.auto_sync }, 'Subscribing to resources...');
|
|
36
|
+
|
|
37
|
+
// Subscribe to resources
|
|
38
|
+
const subResult = await apiClient.subscribe(
|
|
39
|
+
typedParams.resource_ids,
|
|
40
|
+
typedParams.auto_sync
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
logger.info({ count: subResult.subscriptions.length }, 'Resources subscribed successfully');
|
|
44
|
+
|
|
45
|
+
// Auto-sync newly subscribed resources immediately (default: true)
|
|
46
|
+
const shouldAutoSync = typedParams.auto_sync !== false;
|
|
47
|
+
let syncSummary: string | undefined;
|
|
48
|
+
let syncDetails: Array<{ id: string; name: string; action: string }> | undefined;
|
|
49
|
+
let pendingSetup: unknown[] | undefined;
|
|
50
|
+
|
|
51
|
+
if (shouldAutoSync && subResult.subscriptions.length > 0) {
|
|
52
|
+
logger.info({ resourceIds: typedParams.resource_ids }, 'Auto-syncing newly subscribed resources...');
|
|
53
|
+
const syncResult = await syncResources({ mode: 'incremental', scope: typedParams.scope || 'global' });
|
|
54
|
+
if (syncResult.success && syncResult.data) {
|
|
55
|
+
const sd = syncResult.data;
|
|
56
|
+
syncSummary = `Auto-sync: ${sd.summary.synced} synced, ${sd.summary.cached} cached, ${sd.summary.failed} failed`;
|
|
57
|
+
syncDetails = sd.details.map(d => ({ id: d.id, name: d.name, action: d.action }));
|
|
58
|
+
if (sd.pending_setup && sd.pending_setup.length > 0) {
|
|
59
|
+
pendingSetup = sd.pending_setup;
|
|
60
|
+
}
|
|
61
|
+
logger.info({ summary: sd.summary }, 'Auto-sync after subscribe completed');
|
|
62
|
+
} else {
|
|
63
|
+
logger.warn({ error: syncResult.error }, 'Auto-sync after subscribe failed, subscription still recorded');
|
|
64
|
+
syncSummary = 'Auto-sync failed — run sync_resources manually if needed';
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
result = {
|
|
69
|
+
action: 'subscribe',
|
|
70
|
+
success: true,
|
|
71
|
+
subscriptions: subResult.subscriptions.map(sub => ({
|
|
72
|
+
id: sub.id,
|
|
73
|
+
name: sub.name,
|
|
74
|
+
type: sub.type,
|
|
75
|
+
subscribed_at: sub.subscribed_at,
|
|
76
|
+
})),
|
|
77
|
+
message: [
|
|
78
|
+
`Successfully subscribed to ${subResult.subscriptions.length} resource${subResult.subscriptions.length > 1 ? 's' : ''}.`,
|
|
79
|
+
syncSummary,
|
|
80
|
+
].filter(Boolean).join(' '),
|
|
81
|
+
...(syncDetails ? { sync_details: syncDetails } : {}),
|
|
82
|
+
...(pendingSetup ? { pending_setup: pendingSetup } : {}),
|
|
83
|
+
};
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
case 'unsubscribe': {
|
|
88
|
+
// Validate resource_ids
|
|
89
|
+
if (!typedParams.resource_ids || typedParams.resource_ids.length === 0) {
|
|
90
|
+
throw createValidationError(
|
|
91
|
+
'resource_ids',
|
|
92
|
+
'array',
|
|
93
|
+
'resource_ids is required for unsubscribe action'
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
logger.debug({ resourceIds: typedParams.resource_ids }, 'Unsubscribing from resources...');
|
|
98
|
+
|
|
99
|
+
// Cancel server-side subscription
|
|
100
|
+
await apiClient.unsubscribe(typedParams.resource_ids);
|
|
101
|
+
logger.info({ count: typedParams.resource_ids.length }, 'Server-side subscriptions removed');
|
|
102
|
+
|
|
103
|
+
// Uninstall local files and MCP config for each resource
|
|
104
|
+
const uninstallResults: Array<{ id: string; removed: boolean; detail: string }> = [];
|
|
105
|
+
for (const resourceId of typedParams.resource_ids) {
|
|
106
|
+
// Use the last segment of the resource ID as the search pattern
|
|
107
|
+
// e.g. "mcp-client-sdk-ai-hub-jenkins" → "jenkins"
|
|
108
|
+
// "rule-csp-elliotTest" → "elliotTest"
|
|
109
|
+
const namePart = resourceId.split('-').slice(-1)[0] ||
|
|
110
|
+
resourceId.split('-').slice(-2).join('-') ||
|
|
111
|
+
resourceId;
|
|
112
|
+
|
|
113
|
+
// Try full name match first (e.g. "elliotTest"), fallback to last segment
|
|
114
|
+
const patternsToTry = Array.from(new Set([
|
|
115
|
+
resourceId, // full id
|
|
116
|
+
resourceId.replace(/^(skill|cmd|rule|mcp)-[^-]+-/, ''), // strip prefix+source
|
|
117
|
+
namePart,
|
|
118
|
+
]));
|
|
119
|
+
|
|
120
|
+
let uninstalled = false;
|
|
121
|
+
for (const pattern of patternsToTry) {
|
|
122
|
+
const uninstallResult = await uninstallResource({
|
|
123
|
+
resource_id_or_name: pattern,
|
|
124
|
+
remove_from_account: false, // already unsubscribed above
|
|
125
|
+
});
|
|
126
|
+
if (uninstallResult.success && uninstallResult.data && uninstallResult.data.removed_resources.length > 0) {
|
|
127
|
+
uninstallResults.push({ id: resourceId, removed: true, detail: `Removed local files for "${pattern}"` });
|
|
128
|
+
uninstalled = true;
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (!uninstalled) {
|
|
133
|
+
uninstallResults.push({ id: resourceId, removed: false, detail: 'No local files found (may not have been installed)' });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const removedCount = uninstallResults.filter(r => r.removed).length;
|
|
138
|
+
const notFoundCount = uninstallResults.filter(r => !r.removed).length;
|
|
139
|
+
|
|
140
|
+
result = {
|
|
141
|
+
action: 'unsubscribe',
|
|
142
|
+
success: true,
|
|
143
|
+
message: [
|
|
144
|
+
`Successfully unsubscribed from ${typedParams.resource_ids.length} resource${typedParams.resource_ids.length > 1 ? 's' : ''}.`,
|
|
145
|
+
removedCount > 0 ? `Removed local files for ${removedCount} resource${removedCount > 1 ? 's' : ''}.` : null,
|
|
146
|
+
notFoundCount > 0 ? `${notFoundCount} resource${notFoundCount > 1 ? 's were' : ' was'} not installed locally.` : null,
|
|
147
|
+
].filter(Boolean).join(' '),
|
|
148
|
+
sync_details: uninstallResults.map(r => ({ id: r.id, name: r.id, action: r.removed ? 'uninstalled' : 'not_found_locally' })),
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
logger.info({ count: typedParams.resource_ids.length, removedCount }, 'Resources unsubscribed and local files cleaned up');
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
case 'list': {
|
|
156
|
+
logger.debug({ scope: typedParams.scope || 'all' }, 'Listing subscriptions...');
|
|
157
|
+
|
|
158
|
+
// Get subscriptions list
|
|
159
|
+
const subs = await apiClient.getSubscriptions({});
|
|
160
|
+
|
|
161
|
+
result = {
|
|
162
|
+
action: 'list',
|
|
163
|
+
success: true,
|
|
164
|
+
subscriptions: subs.subscriptions.map(sub => ({
|
|
165
|
+
id: sub.id,
|
|
166
|
+
name: sub.name,
|
|
167
|
+
type: sub.type,
|
|
168
|
+
subscribed_at: sub.subscribed_at,
|
|
169
|
+
})),
|
|
170
|
+
message: `Found ${subs.total} subscription${subs.total !== 1 ? 's' : ''}`,
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
logger.info({ total: subs.total }, 'Subscriptions listed successfully');
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
case 'batch_subscribe': {
|
|
178
|
+
// Validate resource_ids
|
|
179
|
+
if (!typedParams.resource_ids || typedParams.resource_ids.length === 0) {
|
|
180
|
+
throw createValidationError(
|
|
181
|
+
'resource_ids',
|
|
182
|
+
'array',
|
|
183
|
+
'resource_ids is required for batch_subscribe action'
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
logger.debug({ count: typedParams.resource_ids.length, autoSync: typedParams.auto_sync }, 'Batch subscribing to resources...');
|
|
188
|
+
|
|
189
|
+
const batchSubResult = await apiClient.subscribe(
|
|
190
|
+
typedParams.resource_ids,
|
|
191
|
+
typedParams.auto_sync
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
logger.info({ count: batchSubResult.subscriptions.length }, 'Batch subscription completed');
|
|
195
|
+
|
|
196
|
+
// Auto-sync newly subscribed resources immediately (default: true)
|
|
197
|
+
const shouldBatchAutoSync = typedParams.auto_sync !== false;
|
|
198
|
+
let batchSyncSummary: string | undefined;
|
|
199
|
+
let batchSyncDetails: Array<{ id: string; name: string; action: string }> | undefined;
|
|
200
|
+
let batchPendingSetup: unknown[] | undefined;
|
|
201
|
+
|
|
202
|
+
if (shouldBatchAutoSync && batchSubResult.subscriptions.length > 0) {
|
|
203
|
+
logger.info({ count: batchSubResult.subscriptions.length }, 'Auto-syncing batch subscribed resources...');
|
|
204
|
+
const batchSyncResult = await syncResources({ mode: 'incremental', scope: typedParams.scope || 'global' });
|
|
205
|
+
if (batchSyncResult.success && batchSyncResult.data) {
|
|
206
|
+
const sd = batchSyncResult.data;
|
|
207
|
+
batchSyncSummary = `Auto-sync: ${sd.summary.synced} synced, ${sd.summary.cached} cached, ${sd.summary.failed} failed`;
|
|
208
|
+
batchSyncDetails = sd.details.map(d => ({ id: d.id, name: d.name, action: d.action }));
|
|
209
|
+
if (sd.pending_setup && sd.pending_setup.length > 0) {
|
|
210
|
+
batchPendingSetup = sd.pending_setup;
|
|
211
|
+
}
|
|
212
|
+
} else {
|
|
213
|
+
batchSyncSummary = 'Auto-sync failed — run sync_resources manually if needed';
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
result = {
|
|
218
|
+
action: 'batch_subscribe',
|
|
219
|
+
success: true,
|
|
220
|
+
subscriptions: batchSubResult.subscriptions.map(sub => ({
|
|
221
|
+
id: sub.id,
|
|
222
|
+
name: sub.name,
|
|
223
|
+
type: sub.type,
|
|
224
|
+
subscribed_at: sub.subscribed_at,
|
|
225
|
+
})),
|
|
226
|
+
message: [
|
|
227
|
+
`Successfully batch subscribed to ${batchSubResult.subscriptions.length} resource${batchSubResult.subscriptions.length > 1 ? 's' : ''}.`,
|
|
228
|
+
batchSyncSummary,
|
|
229
|
+
].filter(Boolean).join(' '),
|
|
230
|
+
...(batchSyncDetails ? { sync_details: batchSyncDetails } : {}),
|
|
231
|
+
...(batchPendingSetup ? { pending_setup: batchPendingSetup } : {}),
|
|
232
|
+
};
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
case 'batch_unsubscribe': {
|
|
237
|
+
// Validate resource_ids
|
|
238
|
+
if (!typedParams.resource_ids || typedParams.resource_ids.length === 0) {
|
|
239
|
+
throw createValidationError(
|
|
240
|
+
'resource_ids',
|
|
241
|
+
'array',
|
|
242
|
+
'resource_ids is required for batch_unsubscribe action'
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
logger.debug({ count: typedParams.resource_ids.length }, 'Batch unsubscribing from resources...');
|
|
247
|
+
|
|
248
|
+
// Delegate entirely to the unsubscribe case for unified cleanup logic
|
|
249
|
+
return manageSubscription({ ...typedParams, action: 'unsubscribe' });
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
default: {
|
|
253
|
+
throw createValidationError(
|
|
254
|
+
'action',
|
|
255
|
+
'string',
|
|
256
|
+
`Unknown action. Must be one of: subscribe, unsubscribe, list, batch_subscribe, batch_unsubscribe`
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const duration = Date.now() - startTime;
|
|
262
|
+
logToolCall('manage_subscription', 'user-id', params as Record<string, unknown>, duration);
|
|
263
|
+
|
|
264
|
+
logger.info(
|
|
265
|
+
{
|
|
266
|
+
action: typedParams.action,
|
|
267
|
+
duration,
|
|
268
|
+
},
|
|
269
|
+
'manage_subscription completed successfully'
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
success: true,
|
|
274
|
+
data: result,
|
|
275
|
+
};
|
|
276
|
+
} catch (error) {
|
|
277
|
+
logger.error({ error, action: typedParams.action }, 'manage_subscription failed');
|
|
278
|
+
return {
|
|
279
|
+
success: false,
|
|
280
|
+
error: {
|
|
281
|
+
code: error instanceof MCPServerError ? error.code : 'UNKNOWN_ERROR',
|
|
282
|
+
message: error instanceof Error ? error.message : String(error),
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Tool definition for registry
|
|
289
|
+
export const manageSubscriptionTool = {
|
|
290
|
+
name: 'manage_subscription',
|
|
291
|
+
description:
|
|
292
|
+
'Manage resource subscriptions (subscribe, unsubscribe, list). ' +
|
|
293
|
+
'When action is "subscribe" or "batch_subscribe", the tool automatically syncs ' +
|
|
294
|
+
'the newly subscribed resources to the local machine immediately after subscribing ' +
|
|
295
|
+
'(auto_sync defaults to true). Pass auto_sync: false only when the user explicitly ' +
|
|
296
|
+
'says they do NOT want the resource installed right now.',
|
|
297
|
+
inputSchema: {
|
|
298
|
+
type: 'object' as const,
|
|
299
|
+
properties: {
|
|
300
|
+
action: {
|
|
301
|
+
type: 'string',
|
|
302
|
+
description: 'Action to perform',
|
|
303
|
+
enum: ['subscribe', 'unsubscribe', 'list', 'batch_subscribe', 'batch_unsubscribe'],
|
|
304
|
+
},
|
|
305
|
+
resource_ids: {
|
|
306
|
+
type: 'array',
|
|
307
|
+
description: 'Resource IDs (required for subscribe/unsubscribe actions)',
|
|
308
|
+
},
|
|
309
|
+
auto_sync: {
|
|
310
|
+
type: 'boolean',
|
|
311
|
+
description:
|
|
312
|
+
'Whether to immediately sync (install) the subscribed resources to the local machine after subscribing. ' +
|
|
313
|
+
'Defaults to true — omit this field in normal usage. ' +
|
|
314
|
+
'Set to false only when the user explicitly says they want to subscribe but NOT install yet.',
|
|
315
|
+
default: true,
|
|
316
|
+
},
|
|
317
|
+
scope: {
|
|
318
|
+
type: 'string',
|
|
319
|
+
description: 'Installation scope',
|
|
320
|
+
enum: ['global', 'workspace'],
|
|
321
|
+
default: 'global',
|
|
322
|
+
},
|
|
323
|
+
notify: {
|
|
324
|
+
type: 'boolean',
|
|
325
|
+
description: 'Enable update notifications',
|
|
326
|
+
default: true,
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
required: ['action'],
|
|
330
|
+
},
|
|
331
|
+
handler: manageSubscription,
|
|
332
|
+
};
|