gitlab-mcp-agent-server 0.2.7 → 0.2.9

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.
@@ -11,6 +11,7 @@ class GitLabOAuthManager {
11
11
  options;
12
12
  tokenStore;
13
13
  oauthBaseUrl;
14
+ pendingOauth;
14
15
  constructor(options) {
15
16
  this.options = options;
16
17
  this.tokenStore = new oauth_token_store_1.OAuthTokenStore(options.tokenStorePath);
@@ -43,9 +44,80 @@ class GitLabOAuthManager {
43
44
  if (!this.options.autoLogin) {
44
45
  throw new errors_1.ConfigurationError('OAuth token is missing. Enable GITLAB_OAUTH_AUTO_LOGIN or provide GITLAB_OAUTH_ACCESS_TOKEN.');
45
46
  }
46
- const interactiveToken = await this.loginInteractivelyWithLock();
47
- this.tokenStore.write(interactiveToken);
48
- return interactiveToken.accessToken;
47
+ const start = await this.startOAuthAuthorization();
48
+ if (start.status === 'already_authorized') {
49
+ const token = this.tokenStore.read();
50
+ if (token?.accessToken) {
51
+ return token.accessToken;
52
+ }
53
+ }
54
+ if (start.status === 'started' || start.status === 'in_progress') {
55
+ throw new errors_1.ConfigurationError(`OAuth authorization is required. Open: ${start.localEntryUrl} (or direct: ${start.authorizeUrl}), complete authorization, then retry the tool call.`);
56
+ }
57
+ if (start.status === 'waiting_other_process') {
58
+ throw new errors_1.ConfigurationError(`OAuth flow is running in another process for this instance. ${start.message} Lock: ${start.lockFilePath}`);
59
+ }
60
+ throw new errors_1.ConfigurationError(start.message);
61
+ }
62
+ async startOAuthAuthorization() {
63
+ const stored = this.tokenStore.read();
64
+ if (stored && !isExpiringSoon(stored.expiresAt)) {
65
+ return {
66
+ status: 'already_authorized',
67
+ message: 'OAuth token already exists and is valid.'
68
+ };
69
+ }
70
+ if (this.pendingOauth) {
71
+ return {
72
+ status: 'in_progress',
73
+ message: 'OAuth authorization is already in progress in this process.',
74
+ localEntryUrl: this.pendingOauth.localEntryUrl,
75
+ authorizeUrl: this.pendingOauth.authorizeUrl
76
+ };
77
+ }
78
+ const lockFilePath = `${this.options.tokenStorePath}.oauth.lock`;
79
+ const lock = acquireOauthLock(lockFilePath);
80
+ if (!lock.acquired) {
81
+ return {
82
+ status: 'waiting_other_process',
83
+ message: 'OAuth flow is already running in another process for this instance.',
84
+ lockFilePath
85
+ };
86
+ }
87
+ let links;
88
+ const promise = this.loginInteractively((readyLinks) => {
89
+ links = readyLinks;
90
+ })
91
+ .then((token) => {
92
+ this.tokenStore.write(token);
93
+ })
94
+ .finally(() => {
95
+ lock.release();
96
+ this.pendingOauth = undefined;
97
+ });
98
+ // Wait a short time until listener is ready and URLs are known.
99
+ const maxReadyWaitMs = 2_000;
100
+ const pollMs = 100;
101
+ let waited = 0;
102
+ while (!links && waited < maxReadyWaitMs) {
103
+ await sleep(pollMs);
104
+ waited += pollMs;
105
+ }
106
+ if (!links) {
107
+ lock.release();
108
+ throw new Error('Failed to start OAuth callback listener.');
109
+ }
110
+ this.pendingOauth = {
111
+ ...links,
112
+ startedAt: new Date().toISOString(),
113
+ promise
114
+ };
115
+ return {
116
+ status: 'started',
117
+ message: 'Open the provided URL and complete OAuth authorization.',
118
+ localEntryUrl: links.localEntryUrl,
119
+ authorizeUrl: links.authorizeUrl
120
+ };
49
121
  }
50
122
  async loginInteractivelyWithLock() {
51
123
  const lockFilePath = `${this.options.tokenStorePath}.oauth.lock`;
@@ -118,7 +190,7 @@ class GitLabOAuthManager {
118
190
  const payload = (await response.json());
119
191
  return mapTokenResponse(payload);
120
192
  }
121
- async loginInteractively() {
193
+ async loginInteractively(onReady) {
122
194
  this.assertOAuthClientCredentials();
123
195
  this.assertRedirectUri();
124
196
  const redirect = new URL(this.options.redirectUri);
@@ -205,6 +277,10 @@ class GitLabOAuthManager {
205
277
  reject(new Error(`OAuth callback server failed on ${redirect.hostname}:${resolvePort(redirect)}: ${error.message}`));
206
278
  });
207
279
  server.listen(resolvePort(redirect), redirect.hostname, () => {
280
+ onReady?.({
281
+ localEntryUrl,
282
+ authorizeUrl: authorizeUrl.toString()
283
+ });
208
284
  const opened = this.options.openBrowser && openInBrowser(localEntryUrl);
209
285
  if (!opened) {
210
286
  console.error('Open this local URL to start OAuth authorization:');
@@ -33,6 +33,7 @@ function createMcpServer() {
33
33
  openBrowser: config.gitlab.oauth.openBrowser
34
34
  })
35
35
  : new token_provider_1.StaticTokenProvider(config.gitlab.accessToken);
36
+ const oauthManager = tokenProvider instanceof gitlab_oauth_manager_1.GitLabOAuthManager ? tokenProvider : undefined;
36
37
  const gitlabApiClient = new gitlab_api_client_1.GitLabApiClient({
37
38
  apiUrl: config.gitlab.apiUrl,
38
39
  tokenProvider
@@ -45,6 +46,7 @@ function createMcpServer() {
45
46
  const issueWorkflowPolicy = new issue_workflow_policy_1.IssueWorkflowPolicy(config);
46
47
  (0, register_tools_1.registerTools)(server, {
47
48
  config,
49
+ oauthManager,
48
50
  projectResolver,
49
51
  issueWorkflowPolicy,
50
52
  healthCheckUseCase: new health_check_1.HealthCheckUseCase(),
@@ -4,6 +4,18 @@ exports.registerTools = registerTools;
4
4
  const zod_1 = require("zod");
5
5
  const errors_1 = require("../../shared/errors");
6
6
  function registerTools(server, deps) {
7
+ if (deps.oauthManager) {
8
+ server.registerTool('gitlab_oauth_start', {
9
+ title: 'GitLab OAuth Start',
10
+ description: 'Starts OAuth authorization flow and returns links to complete authorization in browser.',
11
+ inputSchema: {}
12
+ }, async () => {
13
+ return runTool(async () => {
14
+ const result = await deps.oauthManager?.startOAuthAuthorization();
15
+ return result ?? { status: 'unsupported', message: 'OAuth mode is not enabled.' };
16
+ });
17
+ });
18
+ }
7
19
  server.registerTool('health_check', {
8
20
  title: 'Health Check',
9
21
  description: 'Returns server status.',
@@ -103,8 +103,10 @@ npx -y gitlab-mcp-agent-server
103
103
  3. Если `GITLAB_OAUTH_OPEN_BROWSER=true` и окружение GUI доступно, браузер откроется автоматически.
104
104
  4. Локальный URL `http://127.0.0.1:8787/` автоматически редиректит на GitLab OAuth.
105
105
  5. Если браузер не может быть открыт, сервер печатает URL авторизации в лог.
106
- 6. После подтверждения в GitLab и callback на `http://127.0.0.1:8787/oauth/callback` токены сохраняются в token store для конкретного instance.
107
- 7. В браузере показывается feedback-страница:
106
+ 6. При первом вызове GitLab tool сервер вернет в ответе явную инструкцию с URL для OAuth.
107
+ После завершения авторизации повтори тот же tool-вызов.
108
+ 7. После подтверждения в GitLab и callback на `http://127.0.0.1:8787/oauth/callback` токены сохраняются в token store для конкретного instance.
109
+ 8. В браузере показывается feedback-страница:
108
110
  - `Success`: токен сохранен, можно вернуться в ИИ-агент;
109
111
  - `Error`: показана причина и кнопка повторного запуска OAuth.
110
112
 
@@ -143,6 +145,7 @@ npx -y gitlab-mcp-agent-server
143
145
 
144
146
  ## 8. Быстрая проверка работоспособности
145
147
 
148
+ 0. Если нужен OAuth URL прямо в чате агента, вызови `gitlab_oauth_start`.
146
149
  1. Вызови `gitlab_list_labels`.
147
150
  2. Создай issue: `gitlab_create_issue`.
148
151
  3. Получи issue: `gitlab_get_issue`.
@@ -176,6 +179,7 @@ npx -y gitlab-mcp-agent-server
176
179
  1. Проверь stale lock и удали его:
177
180
  - `rm -f ~/.config/gitlab-mcp/<gitlab-host>/token.json.oauth.lock`
178
181
  2. Повтори запрос и заверши OAuth в браузере в течение окна авторизации.
182
+ 3. Или сначала вызови `gitlab_oauth_start` и открой `localEntryUrl` из ответа.
179
183
 
180
184
  ## 10. Advanced (необязательно)
181
185
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitlab-mcp-agent-server",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "description": "MCP server for GitLab integration via OAuth",
5
5
  "main": "build/src/index.js",
6
6
  "bin": {