@structured-world/gitlab-mcp 6.62.2 → 7.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +22 -1
  2. package/README.md.in +21 -0
  3. package/dist/generated/prisma/internal/class.js +2 -2
  4. package/dist/generated/prisma/internal/prismaNamespace.js +2 -2
  5. package/dist/generated/prisma/models/AuthCodeFlowState.d.ts +1 -2
  6. package/dist/generated/prisma/models/AuthorizationCode.d.ts +1 -2
  7. package/dist/generated/prisma/models/DeviceFlowState.d.ts +1 -2
  8. package/dist/generated/prisma/models/McpSessionMapping.d.ts +1 -2
  9. package/dist/generated/prisma/models/OAuthSession.d.ts +1 -2
  10. package/dist/src/cli/init/connection.js +11 -11
  11. package/dist/src/cli/init/connection.js.map +1 -1
  12. package/dist/src/cli/instances/instances-command.js +5 -1
  13. package/dist/src/cli/instances/instances-command.js.map +1 -1
  14. package/dist/src/config.d.ts +7 -0
  15. package/dist/src/config.js +37 -36
  16. package/dist/src/config.js.map +1 -1
  17. package/dist/src/entities/pipelines/schema-readonly.d.ts +1 -1
  18. package/dist/src/handlers.d.ts +1 -0
  19. package/dist/src/handlers.js +210 -38
  20. package/dist/src/handlers.js.map +1 -1
  21. package/dist/src/logging/types.d.ts +1 -1
  22. package/dist/src/logging/types.js.map +1 -1
  23. package/dist/src/middleware/index.d.ts +1 -0
  24. package/dist/src/middleware/index.js +3 -1
  25. package/dist/src/middleware/index.js.map +1 -1
  26. package/dist/src/middleware/response-write-timeout.d.ts +2 -0
  27. package/dist/src/middleware/response-write-timeout.js +62 -0
  28. package/dist/src/middleware/response-write-timeout.js.map +1 -0
  29. package/dist/src/oauth/gitlab-device-flow.js +31 -26
  30. package/dist/src/oauth/gitlab-device-flow.js.map +1 -1
  31. package/dist/src/registry-manager.d.ts +20 -10
  32. package/dist/src/registry-manager.js +255 -113
  33. package/dist/src/registry-manager.js.map +1 -1
  34. package/dist/src/server.js +37 -21
  35. package/dist/src/server.js.map +1 -1
  36. package/dist/src/services/ConnectionManager.d.ts +29 -20
  37. package/dist/src/services/ConnectionManager.js +265 -140
  38. package/dist/src/services/ConnectionManager.js.map +1 -1
  39. package/dist/src/services/HealthMonitor.d.ts +42 -0
  40. package/dist/src/services/HealthMonitor.js +544 -0
  41. package/dist/src/services/HealthMonitor.js.map +1 -0
  42. package/dist/src/services/InstanceRegistry.d.ts +0 -1
  43. package/dist/src/services/InstanceRegistry.js +9 -21
  44. package/dist/src/services/InstanceRegistry.js.map +1 -1
  45. package/dist/src/services/SchemaIntrospector.d.ts +1 -0
  46. package/dist/src/services/SchemaIntrospector.js +3 -0
  47. package/dist/src/services/SchemaIntrospector.js.map +1 -1
  48. package/dist/src/services/TokenScopeDetector.d.ts +2 -2
  49. package/dist/src/services/TokenScopeDetector.js +59 -36
  50. package/dist/src/services/TokenScopeDetector.js.map +1 -1
  51. package/dist/src/services/ToolAvailability.d.ts +9 -5
  52. package/dist/src/services/ToolAvailability.js +30 -10
  53. package/dist/src/services/ToolAvailability.js.map +1 -1
  54. package/dist/src/services/WidgetAvailability.d.ts +3 -3
  55. package/dist/src/services/WidgetAvailability.js +7 -6
  56. package/dist/src/services/WidgetAvailability.js.map +1 -1
  57. package/dist/src/utils/error-handler.d.ts +10 -1
  58. package/dist/src/utils/error-handler.js +85 -0
  59. package/dist/src/utils/error-handler.js.map +1 -1
  60. package/dist/src/utils/fetch.d.ts +7 -0
  61. package/dist/src/utils/fetch.js +53 -39
  62. package/dist/src/utils/fetch.js.map +1 -1
  63. package/dist/src/utils/gitlab-api.js +5 -2
  64. package/dist/src/utils/gitlab-api.js.map +1 -1
  65. package/dist/src/utils/url.d.ts +1 -0
  66. package/dist/src/utils/url.js +38 -0
  67. package/dist/src/utils/url.js.map +1 -0
  68. package/dist/tsconfig.build.tsbuildinfo +1 -1
  69. package/package.json +19 -25
  70. package/dist/structured-world-gitlab-mcp-6.62.2.tgz +0 -0
@@ -9,38 +9,59 @@ const config_1 = require("../config");
9
9
  const index_1 = require("../oauth/index");
10
10
  const fetch_1 = require("../utils/fetch");
11
11
  const logger_1 = require("../logger");
12
- const InstanceRegistry_js_1 = require("./InstanceRegistry.js");
12
+ const InstanceRegistry_1 = require("./InstanceRegistry");
13
+ const url_1 = require("../utils/url");
13
14
  class ConnectionManager {
14
15
  static instance = null;
15
- client = null;
16
- versionDetector = null;
17
- schemaIntrospector = null;
18
- instanceInfo = null;
19
- schemaInfo = null;
20
- tokenScopeInfo = null;
21
- isInitialized = false;
16
+ instances = new Map();
22
17
  currentInstanceUrl = null;
23
- introspectedInstanceUrl = null;
24
18
  introspectionPromises = new Map();
25
19
  static introspectionCache = new Map();
26
20
  static CACHE_TTL = 10 * 60 * 1000;
21
+ initializePromises = new Map();
22
+ latestRequestedUrl = null;
27
23
  constructor() { }
28
24
  static getInstance() {
29
25
  ConnectionManager.instance ??= new ConnectionManager();
30
26
  return ConnectionManager.instance;
31
27
  }
32
28
  async initialize(instanceUrl) {
33
- if (this.isInitialized) {
29
+ const url = (0, url_1.normalizeInstanceUrl)(instanceUrl ?? config_1.GITLAB_BASE_URL);
30
+ this.latestRequestedUrl = url;
31
+ const existing = this.instances.get(url);
32
+ if (existing?.isInitialized) {
33
+ this.currentInstanceUrl = url;
34
34
  return;
35
35
  }
36
+ const inflight = this.initializePromises.get(url);
37
+ if (inflight) {
38
+ return inflight;
39
+ }
40
+ const promise = this.doInitialize(url);
41
+ this.initializePromises.set(url, promise);
42
+ try {
43
+ await promise;
44
+ const isOurPromise = this.initializePromises.get(url) === promise;
45
+ const initSucceeded = this.instances.get(url)?.isInitialized === true;
46
+ if ((isOurPromise && url === this.latestRequestedUrl) ||
47
+ (!this.currentInstanceUrl && initSucceeded)) {
48
+ this.currentInstanceUrl = url;
49
+ }
50
+ }
51
+ finally {
52
+ if (this.initializePromises.get(url) === promise) {
53
+ this.initializePromises.delete(url);
54
+ }
55
+ }
56
+ }
57
+ async doInitialize(baseUrl) {
58
+ let state;
36
59
  try {
37
60
  const oauthMode = (0, index_1.isOAuthEnabled)();
38
- const registry = InstanceRegistry_js_1.InstanceRegistry.getInstance();
61
+ const registry = InstanceRegistry_1.InstanceRegistry.getInstance();
39
62
  if (!registry.isInitialized()) {
40
63
  await registry.initialize();
41
64
  }
42
- const baseUrl = instanceUrl ?? config_1.GITLAB_BASE_URL;
43
- this.currentInstanceUrl = baseUrl;
44
65
  if (!baseUrl) {
45
66
  throw new Error('GitLab base URL is required');
46
67
  }
@@ -54,19 +75,33 @@ class ConnectionManager {
54
75
  const clientOptions = oauthMode
55
76
  ? {}
56
77
  : { headers: { 'PRIVATE-TOKEN': String(config_1.GITLAB_TOKEN) } };
57
- this.client = new client_1.GraphQLClient(endpoint, clientOptions);
58
- this.versionDetector = new GitLabVersionDetector_1.GitLabVersionDetector(this.client);
59
- this.schemaIntrospector = new SchemaIntrospector_1.SchemaIntrospector(this.client);
78
+ const client = new client_1.GraphQLClient(endpoint, clientOptions);
79
+ const versionDetector = new GitLabVersionDetector_1.GitLabVersionDetector(client);
80
+ const schemaIntrospector = new SchemaIntrospector_1.SchemaIntrospector(client);
81
+ state = {
82
+ client,
83
+ versionDetector,
84
+ schemaIntrospector,
85
+ instanceInfo: null,
86
+ schemaInfo: null,
87
+ tokenScopeInfo: null,
88
+ isInitialized: false,
89
+ introspectedInstanceUrl: null,
90
+ };
91
+ this.instances.set(baseUrl, state);
60
92
  if (oauthMode) {
61
93
  (0, logger_1.logInfo)('OAuth mode: attempting unauthenticated version detection');
62
94
  try {
63
- const versionResponse = await fetch(`${baseUrl}/api/v4/version`);
95
+ const versionResponse = await (0, fetch_1.enhancedFetch)(`${baseUrl}/api/v4/version`, {
96
+ retry: false,
97
+ skipAuth: true,
98
+ });
64
99
  if (versionResponse.ok) {
65
100
  const versionData = (await versionResponse.json());
66
101
  (0, logger_1.logInfo)('Detected GitLab version without authentication', {
67
102
  version: versionData.version,
68
103
  });
69
- this.instanceInfo = {
104
+ state.instanceInfo = {
70
105
  version: versionData.version,
71
106
  tier: versionData.enterprise ? 'premium' : 'free',
72
107
  features: this.getDefaultFeatures(versionData.enterprise ?? false),
@@ -85,36 +120,43 @@ class ConnectionManager {
85
120
  error: error instanceof Error ? error.message : String(error),
86
121
  });
87
122
  }
88
- this.isInitialized = true;
123
+ if (this.instances.get(baseUrl) !== state)
124
+ return;
125
+ state.isInitialized = true;
89
126
  return;
90
127
  }
91
- this.tokenScopeInfo = await (0, TokenScopeDetector_1.detectTokenScopes)();
92
- if (this.tokenScopeInfo) {
128
+ state.tokenScopeInfo = await (0, TokenScopeDetector_1.detectTokenScopes)(baseUrl);
129
+ if (state.tokenScopeInfo) {
93
130
  const totalTools = Object.keys((0, TokenScopeDetector_1.getToolScopeRequirements)()).length;
94
- (0, TokenScopeDetector_1.logTokenScopeInfo)(this.tokenScopeInfo, totalTools);
95
- if (!this.tokenScopeInfo.hasGraphQLAccess) {
96
- this.instanceInfo = await this.detectVersionViaREST();
97
- this.isInitialized = true;
131
+ (0, TokenScopeDetector_1.logTokenScopeInfo)(state.tokenScopeInfo, totalTools, baseUrl);
132
+ if (!state.tokenScopeInfo.hasGraphQLAccess) {
133
+ state.instanceInfo = await this.detectVersionViaREST(baseUrl);
134
+ state.isInitialized = true;
98
135
  return;
99
136
  }
100
137
  }
101
138
  const cached = ConnectionManager.introspectionCache.get(endpoint);
102
139
  const now = Date.now();
103
140
  if (cached && now - cached.timestamp < ConnectionManager.CACHE_TTL) {
141
+ if (this.instances.get(baseUrl) !== state)
142
+ return;
104
143
  (0, logger_1.logInfo)('Using cached GraphQL introspection data');
105
- this.instanceInfo = cached.instanceInfo;
106
- this.schemaInfo = cached.schemaInfo;
107
- this.introspectedInstanceUrl = baseUrl;
144
+ state.instanceInfo = cached.instanceInfo;
145
+ state.schemaInfo = cached.schemaInfo;
146
+ state.schemaIntrospector.rehydrate(cached.schemaInfo);
147
+ state.introspectedInstanceUrl = baseUrl;
108
148
  }
109
149
  else {
110
150
  (0, logger_1.logDebug)('Introspecting GitLab GraphQL schema...');
111
151
  const [instanceInfo, schemaInfo] = await Promise.all([
112
- this.versionDetector.detectInstance(),
113
- this.schemaIntrospector.introspectSchema(),
152
+ versionDetector.detectInstance(),
153
+ schemaIntrospector.introspectSchema(),
114
154
  ]);
115
- this.instanceInfo = instanceInfo;
116
- this.schemaInfo = schemaInfo;
117
- this.introspectedInstanceUrl = baseUrl;
155
+ if (this.instances.get(baseUrl) !== state)
156
+ return;
157
+ state.instanceInfo = instanceInfo;
158
+ state.schemaInfo = schemaInfo;
159
+ state.introspectedInstanceUrl = baseUrl;
118
160
  ConnectionManager.introspectionCache.set(endpoint, {
119
161
  instanceInfo,
120
162
  schemaInfo,
@@ -122,30 +164,37 @@ class ConnectionManager {
122
164
  });
123
165
  (0, logger_1.logInfo)('GraphQL schema introspection completed');
124
166
  }
125
- this.isInitialized = true;
167
+ state.isInitialized = true;
126
168
  (0, logger_1.logInfo)('GitLab instance and schema detected', {
127
- version: this.instanceInfo?.version,
128
- tier: this.instanceInfo?.tier,
129
- features: this.instanceInfo
130
- ? Object.entries(this.instanceInfo.features)
169
+ version: state.instanceInfo?.version,
170
+ tier: state.instanceInfo?.tier,
171
+ features: state.instanceInfo
172
+ ? Object.entries(state.instanceInfo.features)
131
173
  .filter(([, enabled]) => enabled)
132
174
  .map(([feature]) => feature)
133
175
  : [],
134
- widgetTypes: this.schemaInfo?.workItemWidgetTypes.length || 0,
135
- schemaTypes: this.schemaInfo?.typeDefinitions.size || 0,
176
+ widgetTypes: state.schemaInfo?.workItemWidgetTypes.length || 0,
177
+ schemaTypes: state.schemaInfo?.typeDefinitions.size || 0,
136
178
  });
137
179
  }
138
180
  catch (error) {
181
+ if (state && this.instances.get(baseUrl) === state) {
182
+ this.instances.delete(baseUrl);
183
+ }
139
184
  (0, logger_1.logError)('Failed to initialize connection', { err: error });
140
185
  throw error;
141
186
  }
142
187
  }
143
- async ensureIntrospected() {
144
- if (!this.client || !this.versionDetector || !this.schemaIntrospector) {
188
+ async ensureIntrospected(explicitUrl) {
189
+ const instanceUrl = (0, url_1.normalizeInstanceUrl)(explicitUrl ?? (0, index_1.getGitLabApiUrlFromContext)() ?? this.currentInstanceUrl ?? config_1.GITLAB_BASE_URL);
190
+ const state = this.instances.get(instanceUrl);
191
+ if (!state?.client || !state.versionDetector || !state.schemaIntrospector) {
145
192
  throw new Error('Connection not initialized. Call initialize() first.');
146
193
  }
147
- const instanceUrl = (0, index_1.getGitLabApiUrlFromContext)() ?? this.currentInstanceUrl ?? config_1.GITLAB_BASE_URL;
148
- if (this.instanceInfo && this.schemaInfo && this.introspectedInstanceUrl === instanceUrl) {
194
+ if (state.instanceInfo && state.schemaInfo && state.introspectedInstanceUrl === instanceUrl) {
195
+ return;
196
+ }
197
+ if (state.tokenScopeInfo && !state.tokenScopeInfo.hasGraphQLAccess) {
149
198
  return;
150
199
  }
151
200
  const existingPromise = this.introspectionPromises.get(instanceUrl);
@@ -160,34 +209,37 @@ class ConnectionManager {
160
209
  await promise;
161
210
  }
162
211
  finally {
163
- this.introspectionPromises.delete(instanceUrl);
212
+ if (this.introspectionPromises.get(instanceUrl) === promise) {
213
+ this.introspectionPromises.delete(instanceUrl);
214
+ }
164
215
  }
165
216
  }
166
217
  async doIntrospection(instanceUrl) {
167
- const client = this.client;
168
- const versionDet = this.versionDetector;
169
- const schemaDet = this.schemaIntrospector;
170
- if (!client || !versionDet || !schemaDet) {
218
+ const state = this.instances.get(instanceUrl);
219
+ if (!state?.client || !state.versionDetector || !state.schemaIntrospector) {
171
220
  throw new Error('Connection not initialized. Call initialize() first.');
172
221
  }
222
+ const { client, versionDetector, schemaIntrospector } = state;
173
223
  const endpoint = client.endpoint;
174
- const registry = InstanceRegistry_js_1.InstanceRegistry.getInstance();
224
+ const registry = InstanceRegistry_1.InstanceRegistry.getInstance();
175
225
  if (registry.isInitialized()) {
176
226
  const cachedIntrospection = registry.getIntrospection(instanceUrl);
177
227
  if (cachedIntrospection) {
178
228
  (0, logger_1.logInfo)('Using cached introspection from InstanceRegistry', { url: instanceUrl });
179
- this.instanceInfo = {
229
+ state.instanceInfo = {
180
230
  version: cachedIntrospection.version,
181
231
  tier: cachedIntrospection.tier,
182
232
  features: cachedIntrospection.features,
183
233
  detectedAt: cachedIntrospection.cachedAt,
184
234
  };
185
- this.schemaInfo = cachedIntrospection.schemaInfo;
186
- this.introspectedInstanceUrl = instanceUrl;
235
+ const restoredSchema = cachedIntrospection.schemaInfo;
236
+ state.schemaInfo = restoredSchema;
237
+ state.schemaIntrospector.rehydrate(restoredSchema);
238
+ state.introspectedInstanceUrl = instanceUrl;
187
239
  return;
188
240
  }
189
241
  }
190
- const primaryCacheKey = instanceUrl ?? endpoint;
242
+ const primaryCacheKey = instanceUrl;
191
243
  const legacyCacheKey = endpoint;
192
244
  let cached = ConnectionManager.introspectionCache.get(primaryCacheKey);
193
245
  if (!cached && primaryCacheKey !== legacyCacheKey) {
@@ -196,29 +248,22 @@ class ConnectionManager {
196
248
  const now = Date.now();
197
249
  if (cached && now - cached.timestamp < ConnectionManager.CACHE_TTL) {
198
250
  (0, logger_1.logInfo)('Using cached GraphQL introspection data');
199
- this.instanceInfo = cached.instanceInfo;
200
- this.schemaInfo = cached.schemaInfo;
201
- this.introspectedInstanceUrl = instanceUrl;
251
+ state.instanceInfo = cached.instanceInfo;
252
+ state.schemaInfo = cached.schemaInfo;
253
+ state.schemaIntrospector.rehydrate(cached.schemaInfo);
254
+ state.introspectedInstanceUrl = instanceUrl;
202
255
  return;
203
256
  }
204
257
  (0, logger_1.logDebug)('Introspecting GitLab GraphQL schema (deferred OAuth mode)...');
205
- let versionDetector = versionDet;
206
- let schemaIntrospector = schemaDet;
207
- if (registry.isInitialized() && instanceUrl !== this.currentInstanceUrl) {
208
- const instanceClient = this.getInstanceClient(instanceUrl);
209
- if (instanceClient !== client) {
210
- versionDetector = new GitLabVersionDetector_1.GitLabVersionDetector(instanceClient);
211
- schemaIntrospector = new SchemaIntrospector_1.SchemaIntrospector(instanceClient);
212
- (0, logger_1.logDebug)('Using per-instance detectors for introspection', { instanceUrl });
213
- }
214
- }
215
258
  const [instanceInfo, schemaInfo] = await Promise.all([
216
259
  versionDetector.detectInstance(),
217
260
  schemaIntrospector.introspectSchema(),
218
261
  ]);
219
- this.instanceInfo = instanceInfo;
220
- this.schemaInfo = schemaInfo;
221
- this.introspectedInstanceUrl = instanceUrl;
262
+ if (this.instances.get(instanceUrl) !== state)
263
+ return;
264
+ state.instanceInfo = instanceInfo;
265
+ state.schemaInfo = schemaInfo;
266
+ state.introspectedInstanceUrl = instanceUrl;
222
267
  ConnectionManager.introspectionCache.set(primaryCacheKey, {
223
268
  instanceInfo,
224
269
  schemaInfo,
@@ -235,93 +280,131 @@ class ConnectionManager {
235
280
  registry.setIntrospection(instanceUrl, cachedIntrospection);
236
281
  }
237
282
  (0, logger_1.logInfo)('GraphQL schema introspection completed (deferred)', {
238
- version: this.instanceInfo?.version,
239
- tier: this.instanceInfo?.tier,
240
- widgetTypes: this.schemaInfo?.workItemWidgetTypes.length || 0,
283
+ version: state.instanceInfo?.version,
284
+ tier: state.instanceInfo?.tier,
285
+ widgetTypes: state.schemaInfo?.workItemWidgetTypes.length || 0,
241
286
  });
242
287
  }
243
- getClient() {
244
- if (!this.client) {
288
+ resolveState(instanceUrl) {
289
+ const url = instanceUrl ? (0, url_1.normalizeInstanceUrl)(instanceUrl) : this.currentInstanceUrl;
290
+ if (!url) {
245
291
  throw new Error('Connection not initialized. Call initialize() first.');
246
292
  }
247
- return this.client;
293
+ const state = this.instances.get(url);
294
+ if (!state) {
295
+ throw new Error(`Connection not initialized for ${url}. Call initialize() first.`);
296
+ }
297
+ return [state, url];
298
+ }
299
+ getClient(instanceUrl) {
300
+ const [state] = this.resolveState(instanceUrl);
301
+ return state.client;
248
302
  }
249
303
  getInstanceClient(instanceUrl, authHeaders) {
250
- const registry = InstanceRegistry_js_1.InstanceRegistry.getInstance();
251
- const targetUrl = instanceUrl ?? (0, index_1.getGitLabApiUrlFromContext)() ?? this.currentInstanceUrl;
304
+ const registry = InstanceRegistry_1.InstanceRegistry.getInstance();
305
+ const rawTargetUrl = instanceUrl ?? (0, index_1.getGitLabApiUrlFromContext)() ?? this.currentInstanceUrl;
306
+ const targetUrl = rawTargetUrl ? (0, url_1.normalizeInstanceUrl)(rawTargetUrl) : null;
252
307
  if (targetUrl && registry.isInitialized() && registry.has(targetUrl)) {
253
308
  const client = registry.getGraphQLClient(targetUrl, authHeaders);
254
309
  if (client) {
255
310
  return client;
256
311
  }
257
312
  }
258
- if (!this.client) {
259
- throw new Error('Connection not initialized. Call initialize() first.');
313
+ if (targetUrl) {
314
+ const state = this.instances.get(targetUrl);
315
+ if (state)
316
+ return state.client;
317
+ throw new Error(`Connection not initialized for ${targetUrl}. Call initialize() first.`);
260
318
  }
261
- return this.client;
319
+ return this.getClient();
262
320
  }
263
- getVersionDetector() {
264
- if (!this.versionDetector) {
265
- throw new Error('Connection not initialized. Call initialize() first.');
266
- }
267
- return this.versionDetector;
321
+ getVersionDetector(instanceUrl) {
322
+ const [state] = this.resolveState(instanceUrl);
323
+ return state.versionDetector;
268
324
  }
269
- getSchemaIntrospector() {
270
- if (!this.schemaIntrospector) {
271
- throw new Error('Connection not initialized. Call initialize() first.');
272
- }
273
- return this.schemaIntrospector;
325
+ getSchemaIntrospector(instanceUrl) {
326
+ const [state] = this.resolveState(instanceUrl);
327
+ return state.schemaIntrospector;
274
328
  }
275
- getInstanceInfo() {
276
- if (!this.instanceInfo) {
277
- throw new Error('Connection not initialized. Call initialize() first.');
329
+ getInstanceInfo(instanceUrl) {
330
+ const [state, resolvedUrl] = this.resolveState(instanceUrl);
331
+ if (!state.instanceInfo) {
332
+ throw new Error(`Instance information is not available for ${resolvedUrl}. Initialization may have completed without version detection (OAuth deferred/REST-only mode).`);
278
333
  }
279
- return this.instanceInfo;
334
+ return state.instanceInfo;
280
335
  }
281
- getSchemaInfo() {
282
- if (!this.schemaInfo) {
283
- throw new Error('Connection not initialized. Call initialize() first.');
336
+ getSchemaInfo(instanceUrl) {
337
+ const [state, resolvedUrl] = this.resolveState(instanceUrl);
338
+ if (!state.schemaInfo) {
339
+ throw new Error(`Schema information is not available for ${resolvedUrl}. Initialization may have completed without schema introspection.`);
284
340
  }
285
- return this.schemaInfo;
341
+ return state.schemaInfo;
286
342
  }
287
343
  getCurrentInstanceUrl() {
288
344
  return this.currentInstanceUrl;
289
345
  }
290
- isFeatureAvailable(feature) {
291
- if (!this.instanceInfo) {
346
+ isConnected(instanceUrl) {
347
+ const url = instanceUrl ? (0, url_1.normalizeInstanceUrl)(instanceUrl) : this.currentInstanceUrl;
348
+ if (!url)
292
349
  return false;
293
- }
294
- return this.instanceInfo.features[feature];
350
+ const state = this.instances.get(url);
351
+ return state?.isInitialized ?? false;
352
+ }
353
+ isFeatureAvailable(feature, instanceUrl) {
354
+ const url = instanceUrl ? (0, url_1.normalizeInstanceUrl)(instanceUrl) : this.currentInstanceUrl;
355
+ if (!url)
356
+ return false;
357
+ const state = this.instances.get(url);
358
+ if (!state?.instanceInfo)
359
+ return false;
360
+ return state.instanceInfo.features[feature];
295
361
  }
296
- getTier() {
297
- if (!this.instanceInfo) {
362
+ getTier(instanceUrl) {
363
+ const url = instanceUrl ? (0, url_1.normalizeInstanceUrl)(instanceUrl) : this.currentInstanceUrl;
364
+ if (!url)
298
365
  return 'unknown';
299
- }
300
- return this.instanceInfo.tier;
366
+ const state = this.instances.get(url);
367
+ if (!state?.instanceInfo)
368
+ return 'unknown';
369
+ return state.instanceInfo.tier;
301
370
  }
302
- getVersion() {
303
- if (!this.instanceInfo) {
371
+ getVersion(instanceUrl) {
372
+ const url = instanceUrl ? (0, url_1.normalizeInstanceUrl)(instanceUrl) : this.currentInstanceUrl;
373
+ if (!url)
304
374
  return 'unknown';
305
- }
306
- return this.instanceInfo.version;
375
+ const state = this.instances.get(url);
376
+ if (!state?.instanceInfo)
377
+ return 'unknown';
378
+ return state.instanceInfo.version;
307
379
  }
308
- isWidgetAvailable(widgetType) {
309
- if (!this.schemaIntrospector) {
380
+ isWidgetAvailable(widgetType, instanceUrl) {
381
+ const url = instanceUrl ? (0, url_1.normalizeInstanceUrl)(instanceUrl) : this.currentInstanceUrl;
382
+ if (!url)
310
383
  return false;
311
- }
312
- return this.schemaIntrospector.isWidgetTypeAvailable(widgetType);
384
+ const state = this.instances.get(url);
385
+ return state?.schemaInfo?.workItemWidgetTypes.includes(widgetType) ?? false;
313
386
  }
314
- getTokenScopeInfo() {
315
- return this.tokenScopeInfo;
387
+ getTokenScopeInfo(instanceUrl) {
388
+ const url = instanceUrl ? (0, url_1.normalizeInstanceUrl)(instanceUrl) : this.currentInstanceUrl;
389
+ if (!url)
390
+ return null;
391
+ const state = this.instances.get(url);
392
+ return state?.tokenScopeInfo ?? null;
316
393
  }
317
394
  async refreshTokenScopes() {
318
395
  if ((0, index_1.isOAuthEnabled)()) {
319
396
  return false;
320
397
  }
321
- const previousScopes = this.tokenScopeInfo?.scopes ?? [];
322
- const previousHasGraphQL = this.tokenScopeInfo?.hasGraphQLAccess ?? false;
323
- const previousHasWrite = this.tokenScopeInfo?.hasWriteAccess ?? false;
324
- const newScopeInfo = await (0, TokenScopeDetector_1.detectTokenScopes)();
398
+ const url = this.currentInstanceUrl;
399
+ if (!url)
400
+ return false;
401
+ const state = this.instances.get(url);
402
+ if (!state)
403
+ return false;
404
+ const previousScopes = state.tokenScopeInfo?.scopes ?? [];
405
+ const previousHasGraphQL = state.tokenScopeInfo?.hasGraphQLAccess ?? false;
406
+ const previousHasWrite = state.tokenScopeInfo?.hasWriteAccess ?? false;
407
+ const newScopeInfo = await (0, TokenScopeDetector_1.detectTokenScopes)(url);
325
408
  if (!newScopeInfo) {
326
409
  return false;
327
410
  }
@@ -330,8 +413,12 @@ class ConnectionManager {
330
413
  !previousScopes.every((s) => newScopes.includes(s)) ||
331
414
  previousHasGraphQL !== newScopeInfo.hasGraphQLAccess ||
332
415
  previousHasWrite !== newScopeInfo.hasWriteAccess;
416
+ const currentState = this.instances.get(url);
417
+ if (currentState !== state) {
418
+ return false;
419
+ }
420
+ state.tokenScopeInfo = newScopeInfo;
333
421
  if (scopesChanged) {
334
- this.tokenScopeInfo = newScopeInfo;
335
422
  (0, logger_1.logInfo)('Token scopes changed - tool registry will be refreshed', {
336
423
  previousScopes,
337
424
  newScopes,
@@ -341,10 +428,10 @@ class ConnectionManager {
341
428
  }
342
429
  return scopesChanged;
343
430
  }
344
- async detectVersionViaREST() {
431
+ async detectVersionViaREST(baseUrl) {
345
432
  try {
346
- const baseUrl = this.currentInstanceUrl ?? config_1.GITLAB_BASE_URL;
347
- const response = await (0, fetch_1.enhancedFetch)(`${baseUrl}/api/v4/version`, {
433
+ const url = baseUrl ?? this.currentInstanceUrl ?? config_1.GITLAB_BASE_URL;
434
+ const response = await (0, fetch_1.enhancedFetch)(`${url}/api/v4/version`, {
348
435
  headers: {
349
436
  'PRIVATE-TOKEN': config_1.GITLAB_TOKEN ?? '',
350
437
  Accept: 'application/json',
@@ -407,31 +494,69 @@ class ConnectionManager {
407
494
  emailParticipants: true,
408
495
  };
409
496
  }
410
- async reinitialize(newInstanceUrl) {
497
+ async reinitialize(rawInstanceUrl) {
498
+ const newInstanceUrl = (0, url_1.normalizeInstanceUrl)(rawInstanceUrl);
411
499
  (0, logger_1.logInfo)('Re-initializing ConnectionManager for new instance', {
412
500
  newInstanceUrl,
413
501
  });
414
- this.reset();
415
- const registry = InstanceRegistry_js_1.InstanceRegistry.getInstance();
416
- registry.clearIntrospectionCache(newInstanceUrl);
417
- await this.initialize(newInstanceUrl);
502
+ const previousUrl = this.currentInstanceUrl;
503
+ const savedState = this.instances.get(newInstanceUrl);
504
+ const restorableState = savedState?.isInitialized ? savedState : undefined;
505
+ this.initializePromises.delete(newInstanceUrl);
506
+ this.introspectionPromises.delete(newInstanceUrl);
507
+ this.instances.delete(newInstanceUrl);
508
+ try {
509
+ const registry = InstanceRegistry_1.InstanceRegistry.getInstance();
510
+ registry.clearIntrospectionCache(newInstanceUrl);
511
+ }
512
+ catch {
513
+ }
514
+ ConnectionManager.introspectionCache.delete(newInstanceUrl);
515
+ ConnectionManager.introspectionCache.delete(`${newInstanceUrl}/api/graphql`);
516
+ try {
517
+ await this.initialize(newInstanceUrl);
518
+ }
519
+ catch (error) {
520
+ if (restorableState) {
521
+ this.instances.set(newInstanceUrl, restorableState);
522
+ }
523
+ if (previousUrl && this.instances.has(previousUrl)) {
524
+ this.currentInstanceUrl = previousUrl;
525
+ }
526
+ else if (restorableState) {
527
+ this.currentInstanceUrl = newInstanceUrl;
528
+ }
529
+ throw error;
530
+ }
531
+ if (previousUrl && previousUrl !== newInstanceUrl) {
532
+ this.initializePromises.delete(previousUrl);
533
+ this.introspectionPromises.delete(previousUrl);
534
+ this.instances.delete(previousUrl);
535
+ }
536
+ const state = this.instances.get(newInstanceUrl);
418
537
  (0, logger_1.logInfo)('ConnectionManager re-initialized', {
419
- version: this.instanceInfo?.version,
420
- tier: this.instanceInfo?.tier,
538
+ version: state?.instanceInfo?.version,
539
+ tier: state?.instanceInfo?.tier,
421
540
  instanceUrl: this.currentInstanceUrl,
422
541
  });
423
542
  }
543
+ clearInflight(rawUrl) {
544
+ const url = (0, url_1.normalizeInstanceUrl)(rawUrl);
545
+ this.initializePromises.delete(url);
546
+ this.introspectionPromises.delete(url);
547
+ }
424
548
  reset() {
425
- this.client = null;
426
- this.versionDetector = null;
427
- this.schemaIntrospector = null;
428
- this.instanceInfo = null;
429
- this.schemaInfo = null;
430
- this.tokenScopeInfo = null;
549
+ this.instances.clear();
431
550
  this.currentInstanceUrl = null;
432
- this.introspectedInstanceUrl = null;
551
+ this.latestRequestedUrl = null;
433
552
  this.introspectionPromises.clear();
434
- this.isInitialized = false;
553
+ this.initializePromises.clear();
554
+ ConnectionManager.introspectionCache.clear();
555
+ try {
556
+ InstanceRegistry_1.InstanceRegistry.getInstance().clearIntrospectionCache();
557
+ }
558
+ catch {
559
+ }
435
560
  }
436
561
  }
437
562
  exports.ConnectionManager = ConnectionManager;