gitlab-mcp-agent-server 0.2.6 → 0.2.8
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);
|
|
@@ -47,6 +48,66 @@ class GitLabOAuthManager {
|
|
|
47
48
|
this.tokenStore.write(interactiveToken);
|
|
48
49
|
return interactiveToken.accessToken;
|
|
49
50
|
}
|
|
51
|
+
async startOAuthAuthorization() {
|
|
52
|
+
const stored = this.tokenStore.read();
|
|
53
|
+
if (stored && !isExpiringSoon(stored.expiresAt)) {
|
|
54
|
+
return {
|
|
55
|
+
status: 'already_authorized',
|
|
56
|
+
message: 'OAuth token already exists and is valid.'
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
if (this.pendingOauth) {
|
|
60
|
+
return {
|
|
61
|
+
status: 'in_progress',
|
|
62
|
+
message: 'OAuth authorization is already in progress in this process.',
|
|
63
|
+
localEntryUrl: this.pendingOauth.localEntryUrl,
|
|
64
|
+
authorizeUrl: this.pendingOauth.authorizeUrl
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const lockFilePath = `${this.options.tokenStorePath}.oauth.lock`;
|
|
68
|
+
const lock = acquireOauthLock(lockFilePath);
|
|
69
|
+
if (!lock.acquired) {
|
|
70
|
+
return {
|
|
71
|
+
status: 'waiting_other_process',
|
|
72
|
+
message: 'OAuth flow is already running in another process for this instance.',
|
|
73
|
+
lockFilePath
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
let links;
|
|
77
|
+
const promise = this.loginInteractively((readyLinks) => {
|
|
78
|
+
links = readyLinks;
|
|
79
|
+
})
|
|
80
|
+
.then((token) => {
|
|
81
|
+
this.tokenStore.write(token);
|
|
82
|
+
})
|
|
83
|
+
.finally(() => {
|
|
84
|
+
lock.release();
|
|
85
|
+
this.pendingOauth = undefined;
|
|
86
|
+
});
|
|
87
|
+
// Wait a short time until listener is ready and URLs are known.
|
|
88
|
+
const maxReadyWaitMs = 2_000;
|
|
89
|
+
const pollMs = 100;
|
|
90
|
+
let waited = 0;
|
|
91
|
+
while (!links && waited < maxReadyWaitMs) {
|
|
92
|
+
await sleep(pollMs);
|
|
93
|
+
waited += pollMs;
|
|
94
|
+
}
|
|
95
|
+
if (!links) {
|
|
96
|
+
lock.release();
|
|
97
|
+
throw new Error('Failed to start OAuth callback listener.');
|
|
98
|
+
}
|
|
99
|
+
this.pendingOauth = {
|
|
100
|
+
...links,
|
|
101
|
+
startedAt: new Date().toISOString(),
|
|
102
|
+
promise
|
|
103
|
+
};
|
|
104
|
+
return {
|
|
105
|
+
status: 'started',
|
|
106
|
+
message: 'Open the provided URL and complete OAuth authorization.',
|
|
107
|
+
localEntryUrl: links.localEntryUrl,
|
|
108
|
+
authorizeUrl: links.authorizeUrl
|
|
109
|
+
};
|
|
110
|
+
}
|
|
50
111
|
async loginInteractivelyWithLock() {
|
|
51
112
|
const lockFilePath = `${this.options.tokenStorePath}.oauth.lock`;
|
|
52
113
|
const lock = acquireOauthLock(lockFilePath);
|
|
@@ -54,6 +115,13 @@ class GitLabOAuthManager {
|
|
|
54
115
|
try {
|
|
55
116
|
return await this.loginInteractively();
|
|
56
117
|
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
if (error instanceof OAuthCallbackPortBusyError) {
|
|
120
|
+
console.error(error.message);
|
|
121
|
+
return this.waitForTokenFromOtherProcess(lockFilePath);
|
|
122
|
+
}
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
57
125
|
finally {
|
|
58
126
|
lock.release();
|
|
59
127
|
}
|
|
@@ -111,7 +179,7 @@ class GitLabOAuthManager {
|
|
|
111
179
|
const payload = (await response.json());
|
|
112
180
|
return mapTokenResponse(payload);
|
|
113
181
|
}
|
|
114
|
-
async loginInteractively() {
|
|
182
|
+
async loginInteractively(onReady) {
|
|
115
183
|
this.assertOAuthClientCredentials();
|
|
116
184
|
this.assertRedirectUri();
|
|
117
185
|
const redirect = new URL(this.options.redirectUri);
|
|
@@ -190,9 +258,18 @@ class GitLabOAuthManager {
|
|
|
190
258
|
});
|
|
191
259
|
server.on('error', (error) => {
|
|
192
260
|
settled = true;
|
|
261
|
+
const errno = error;
|
|
262
|
+
if (errno.code === 'EADDRINUSE') {
|
|
263
|
+
reject(new OAuthCallbackPortBusyError(`OAuth callback port is busy (${redirect.hostname}:${resolvePort(redirect)}). If another OAuth flow is active, finish it and retry.`));
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
193
266
|
reject(new Error(`OAuth callback server failed on ${redirect.hostname}:${resolvePort(redirect)}: ${error.message}`));
|
|
194
267
|
});
|
|
195
268
|
server.listen(resolvePort(redirect), redirect.hostname, () => {
|
|
269
|
+
onReady?.({
|
|
270
|
+
localEntryUrl,
|
|
271
|
+
authorizeUrl: authorizeUrl.toString()
|
|
272
|
+
});
|
|
196
273
|
const opened = this.options.openBrowser && openInBrowser(localEntryUrl);
|
|
197
274
|
if (!opened) {
|
|
198
275
|
console.error('Open this local URL to start OAuth authorization:');
|
|
@@ -315,6 +392,12 @@ class OAuthRefreshError extends Error {
|
|
|
315
392
|
this.name = 'OAuthRefreshError';
|
|
316
393
|
}
|
|
317
394
|
}
|
|
395
|
+
class OAuthCallbackPortBusyError extends Error {
|
|
396
|
+
constructor(message) {
|
|
397
|
+
super(message);
|
|
398
|
+
this.name = 'OAuthCallbackPortBusyError';
|
|
399
|
+
}
|
|
400
|
+
}
|
|
318
401
|
function shouldReloginOnRefreshFailure(error) {
|
|
319
402
|
if (!(error instanceof OAuthRefreshError)) {
|
|
320
403
|
return false;
|
|
@@ -375,6 +458,15 @@ function isStaleLock(lockFilePath) {
|
|
|
375
458
|
try {
|
|
376
459
|
const raw = (0, node_fs_1.readFileSync)(lockFilePath, 'utf8');
|
|
377
460
|
const parsed = JSON.parse(raw);
|
|
461
|
+
if (parsed.startedAt) {
|
|
462
|
+
const startedAtMs = new Date(parsed.startedAt).getTime();
|
|
463
|
+
if (!Number.isNaN(startedAtMs)) {
|
|
464
|
+
const ageMs = Date.now() - startedAtMs;
|
|
465
|
+
if (ageMs > 10 * 60 * 1000) {
|
|
466
|
+
return true;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
378
470
|
if (!parsed.pid || !Number.isInteger(parsed.pid)) {
|
|
379
471
|
return true;
|
|
380
472
|
}
|
|
@@ -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.',
|
package/docs/USER_GUIDE.md
CHANGED
|
@@ -143,6 +143,7 @@ npx -y gitlab-mcp-agent-server
|
|
|
143
143
|
|
|
144
144
|
## 8. Быстрая проверка работоспособности
|
|
145
145
|
|
|
146
|
+
0. Если нужен OAuth URL прямо в чате агента, вызови `gitlab_oauth_start`.
|
|
146
147
|
1. Вызови `gitlab_list_labels`.
|
|
147
148
|
2. Создай issue: `gitlab_create_issue`.
|
|
148
149
|
3. Получи issue: `gitlab_get_issue`.
|
|
@@ -176,6 +177,7 @@ npx -y gitlab-mcp-agent-server
|
|
|
176
177
|
1. Проверь stale lock и удали его:
|
|
177
178
|
- `rm -f ~/.config/gitlab-mcp/<gitlab-host>/token.json.oauth.lock`
|
|
178
179
|
2. Повтори запрос и заверши OAuth в браузере в течение окна авторизации.
|
|
180
|
+
3. Или сначала вызови `gitlab_oauth_start` и открой `localEntryUrl` из ответа.
|
|
179
181
|
|
|
180
182
|
## 10. Advanced (необязательно)
|
|
181
183
|
|