gitlab-mcp-agent-server 0.2.0 → 0.2.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.
- package/build/src/application/services/project-resolver.js +3 -3
- package/build/src/infrastructure/auth/gitlab-oauth-manager.js +3 -0
- package/build/src/infrastructure/auth/group-oauth-token-provider.js +51 -0
- package/build/src/infrastructure/gitlab/gitlab-api-client.js +9 -9
- package/build/src/interface/mcp/create-mcp-server.js +14 -9
- package/build/src/shared/config.js +67 -0
- package/docs/USER_GUIDE.md +8 -2
- package/package.json +1 -1
|
@@ -11,12 +11,12 @@ class ProjectResolver {
|
|
|
11
11
|
if (project !== undefined && project !== null && String(project).trim() !== '') {
|
|
12
12
|
return project;
|
|
13
13
|
}
|
|
14
|
-
if (this.config.gitlab.defaultProject) {
|
|
15
|
-
return this.config.gitlab.defaultProject;
|
|
16
|
-
}
|
|
17
14
|
if (this.config.gitlab.autoResolveProjectFromGit && this.config.gitlab.autoDetectedProject) {
|
|
18
15
|
return this.config.gitlab.autoDetectedProject;
|
|
19
16
|
}
|
|
17
|
+
if (this.config.gitlab.defaultProject) {
|
|
18
|
+
return this.config.gitlab.defaultProject;
|
|
19
|
+
}
|
|
20
20
|
throw new errors_1.ConfigurationError('Project is not resolved. Provide `project` in tool input or set GITLAB_DEFAULT_PROJECT.');
|
|
21
21
|
}
|
|
22
22
|
}
|
|
@@ -111,6 +111,9 @@ class GitLabOAuthManager {
|
|
|
111
111
|
server.close();
|
|
112
112
|
resolve(authCode);
|
|
113
113
|
});
|
|
114
|
+
server.on('error', (error) => {
|
|
115
|
+
reject(new Error(`OAuth callback server failed on ${redirect.hostname}:${resolvePort(redirect)}: ${error.message}`));
|
|
116
|
+
});
|
|
114
117
|
server.listen(resolvePort(redirect), redirect.hostname, () => {
|
|
115
118
|
const opened = this.options.openBrowser && openInBrowser(localEntryUrl);
|
|
116
119
|
if (!opened) {
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GroupOAuthTokenProvider = void 0;
|
|
4
|
+
exports.resolveOAuthGroupKey = resolveOAuthGroupKey;
|
|
5
|
+
const errors_1 = require("../../shared/errors");
|
|
6
|
+
const gitlab_oauth_manager_1 = require("./gitlab-oauth-manager");
|
|
7
|
+
class GroupOAuthTokenProvider {
|
|
8
|
+
options;
|
|
9
|
+
groupProviders;
|
|
10
|
+
constructor(options) {
|
|
11
|
+
this.options = options;
|
|
12
|
+
this.groupProviders = Object.fromEntries(Object.entries(options.groupConfigs).map(([groupKey, cfg]) => {
|
|
13
|
+
return [
|
|
14
|
+
groupKey,
|
|
15
|
+
new gitlab_oauth_manager_1.GitLabOAuthManager({
|
|
16
|
+
apiUrl: options.apiUrl,
|
|
17
|
+
clientId: cfg.clientId,
|
|
18
|
+
clientSecret: cfg.clientSecret,
|
|
19
|
+
redirectUri: cfg.redirectUri,
|
|
20
|
+
scopes: cfg.scopes,
|
|
21
|
+
bootstrapAccessToken: cfg.bootstrapAccessToken,
|
|
22
|
+
tokenStorePath: cfg.tokenStorePath,
|
|
23
|
+
autoLogin: cfg.autoLogin,
|
|
24
|
+
openBrowser: cfg.openBrowser
|
|
25
|
+
})
|
|
26
|
+
];
|
|
27
|
+
}));
|
|
28
|
+
}
|
|
29
|
+
async getAccessToken(projectRef) {
|
|
30
|
+
const groupKey = resolveOAuthGroupKey(projectRef, Object.keys(this.groupProviders));
|
|
31
|
+
if (groupKey) {
|
|
32
|
+
const provider = this.groupProviders[groupKey];
|
|
33
|
+
if (provider) {
|
|
34
|
+
return provider.getAccessToken(projectRef);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (this.options.defaultProvider) {
|
|
38
|
+
return this.options.defaultProvider.getAccessToken(projectRef);
|
|
39
|
+
}
|
|
40
|
+
throw new errors_1.ConfigurationError('No OAuth config matched this project and default OAuth config is not available.');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
exports.GroupOAuthTokenProvider = GroupOAuthTokenProvider;
|
|
44
|
+
function resolveOAuthGroupKey(projectRef, configuredGroups) {
|
|
45
|
+
if (typeof projectRef !== 'string' || projectRef.trim() === '') {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
const normalized = projectRef.trim().replace(/^\/+/, '');
|
|
49
|
+
const sorted = [...configuredGroups].sort((a, b) => b.length - a.length);
|
|
50
|
+
return sorted.find((group) => normalized === group || normalized.startsWith(`${group}/`));
|
|
51
|
+
}
|
|
@@ -22,12 +22,12 @@ class GitLabApiClient {
|
|
|
22
22
|
const data = await this.request(`/projects/${projectPath}/issues`, {
|
|
23
23
|
method: 'POST',
|
|
24
24
|
body: JSON.stringify(body)
|
|
25
|
-
});
|
|
25
|
+
}, input.project);
|
|
26
26
|
return mapIssue(data);
|
|
27
27
|
}
|
|
28
28
|
async getIssue(input) {
|
|
29
29
|
const projectPath = encodeProjectRef(input.project);
|
|
30
|
-
const data = await this.request(`/projects/${projectPath}/issues/${input.issueIid}
|
|
30
|
+
const data = await this.request(`/projects/${projectPath}/issues/${input.issueIid}`, undefined, input.project);
|
|
31
31
|
return mapIssue(data);
|
|
32
32
|
}
|
|
33
33
|
async closeIssue(input) {
|
|
@@ -35,7 +35,7 @@ class GitLabApiClient {
|
|
|
35
35
|
const data = await this.request(`/projects/${projectPath}/issues/${input.issueIid}`, {
|
|
36
36
|
method: 'PUT',
|
|
37
37
|
body: JSON.stringify({ state_event: 'close' })
|
|
38
|
-
});
|
|
38
|
+
}, input.project);
|
|
39
39
|
return mapIssue(data);
|
|
40
40
|
}
|
|
41
41
|
async updateIssueLabels(input) {
|
|
@@ -43,7 +43,7 @@ class GitLabApiClient {
|
|
|
43
43
|
const data = await this.request(`/projects/${projectPath}/issues/${input.issueIid}`, {
|
|
44
44
|
method: 'PUT',
|
|
45
45
|
body: JSON.stringify({ labels: input.labels.join(',') })
|
|
46
|
-
});
|
|
46
|
+
}, input.project);
|
|
47
47
|
return mapIssue(data);
|
|
48
48
|
}
|
|
49
49
|
async listLabels(input) {
|
|
@@ -51,7 +51,7 @@ class GitLabApiClient {
|
|
|
51
51
|
const query = input.search
|
|
52
52
|
? `/projects/${projectPath}/labels?search=${encodeURIComponent(input.search)}`
|
|
53
53
|
: `/projects/${projectPath}/labels`;
|
|
54
|
-
const data = await this.request(query);
|
|
54
|
+
const data = await this.request(query, undefined, input.project);
|
|
55
55
|
return data.map(mapLabel);
|
|
56
56
|
}
|
|
57
57
|
async listIssues(input) {
|
|
@@ -73,7 +73,7 @@ class GitLabApiClient {
|
|
|
73
73
|
query.set('page', String(input.page));
|
|
74
74
|
}
|
|
75
75
|
const querySuffix = query.size > 0 ? `?${query.toString()}` : '';
|
|
76
|
-
const data = await this.request(`/projects/${projectPath}/issues${querySuffix}
|
|
76
|
+
const data = await this.request(`/projects/${projectPath}/issues${querySuffix}`, undefined, input.project);
|
|
77
77
|
return data.map(mapIssue);
|
|
78
78
|
}
|
|
79
79
|
async createLabel(input) {
|
|
@@ -85,11 +85,11 @@ class GitLabApiClient {
|
|
|
85
85
|
color: input.color ?? '#1f75cb',
|
|
86
86
|
description: input.description
|
|
87
87
|
})
|
|
88
|
-
});
|
|
88
|
+
}, input.project);
|
|
89
89
|
return mapLabel(data);
|
|
90
90
|
}
|
|
91
|
-
async request(path, init) {
|
|
92
|
-
const token = await this.options.tokenProvider.getAccessToken();
|
|
91
|
+
async request(path, init, projectRef) {
|
|
92
|
+
const token = await this.options.tokenProvider.getAccessToken(projectRef);
|
|
93
93
|
if (!token) {
|
|
94
94
|
throw new errors_1.ConfigurationError('GitLab access token is not configured. Set GITLAB_OAUTH_ACCESS_TOKEN (oauth) or GITLAB_PAT (pat).');
|
|
95
95
|
}
|
|
@@ -12,6 +12,7 @@ const health_check_1 = require("../../application/use-cases/health-check");
|
|
|
12
12
|
const list_labels_1 = require("../../application/use-cases/list-labels");
|
|
13
13
|
const list_issues_1 = require("../../application/use-cases/list-issues");
|
|
14
14
|
const update_issue_labels_1 = require("../../application/use-cases/update-issue-labels");
|
|
15
|
+
const group_oauth_token_provider_1 = require("../../infrastructure/auth/group-oauth-token-provider");
|
|
15
16
|
const gitlab_oauth_manager_1 = require("../../infrastructure/auth/gitlab-oauth-manager");
|
|
16
17
|
const token_provider_1 = require("../../infrastructure/auth/token-provider");
|
|
17
18
|
const gitlab_api_client_1 = require("../../infrastructure/gitlab/gitlab-api-client");
|
|
@@ -20,16 +21,20 @@ const register_tools_1 = require("./register-tools");
|
|
|
20
21
|
function createMcpServer() {
|
|
21
22
|
const config = (0, config_1.loadConfig)();
|
|
22
23
|
const tokenProvider = config.gitlab.authMode === 'oauth'
|
|
23
|
-
? new
|
|
24
|
+
? new group_oauth_token_provider_1.GroupOAuthTokenProvider({
|
|
24
25
|
apiUrl: config.gitlab.apiUrl,
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
26
|
+
defaultProvider: new gitlab_oauth_manager_1.GitLabOAuthManager({
|
|
27
|
+
apiUrl: config.gitlab.apiUrl,
|
|
28
|
+
clientId: config.gitlab.oauth.clientId,
|
|
29
|
+
clientSecret: config.gitlab.oauth.clientSecret,
|
|
30
|
+
redirectUri: config.gitlab.oauth.redirectUri,
|
|
31
|
+
scopes: config.gitlab.oauth.scopes,
|
|
32
|
+
bootstrapAccessToken: config.gitlab.accessToken,
|
|
33
|
+
tokenStorePath: config.gitlab.oauth.tokenStorePath,
|
|
34
|
+
autoLogin: config.gitlab.oauth.autoLogin,
|
|
35
|
+
openBrowser: config.gitlab.oauth.openBrowser
|
|
36
|
+
}),
|
|
37
|
+
groupConfigs: config.gitlab.groupOAuthConfigs
|
|
33
38
|
})
|
|
34
39
|
: new token_provider_1.StaticTokenProvider(config.gitlab.accessToken);
|
|
35
40
|
const gitlabApiClient = new gitlab_api_client_1.GitLabApiClient({
|
|
@@ -23,6 +23,7 @@ const EnvSchema = zod_1.z.object({
|
|
|
23
23
|
.enum(['true', 'false'])
|
|
24
24
|
.optional()
|
|
25
25
|
.transform((value) => value !== 'false'),
|
|
26
|
+
GITLAB_GROUP_OAUTH_CONFIG_JSON: zod_1.z.string().optional(),
|
|
26
27
|
GITLAB_PAT: zod_1.z.string().optional(),
|
|
27
28
|
GITLAB_DEFAULT_PROJECT: zod_1.z.string().optional(),
|
|
28
29
|
GITLAB_AUTO_RESOLVE_PROJECT_FROM_GIT: zod_1.z
|
|
@@ -70,6 +71,12 @@ function loadConfig() {
|
|
|
70
71
|
autoLogin: env.GITLAB_OAUTH_AUTO_LOGIN,
|
|
71
72
|
openBrowser: env.GITLAB_OAUTH_OPEN_BROWSER
|
|
72
73
|
},
|
|
74
|
+
groupOAuthConfigs: parseGroupOAuthConfigJson(env.GITLAB_GROUP_OAUTH_CONFIG_JSON, {
|
|
75
|
+
redirectUri: env.GITLAB_OAUTH_REDIRECT_URI,
|
|
76
|
+
scopes: splitCsv(env.GITLAB_OAUTH_SCOPES),
|
|
77
|
+
autoLogin: env.GITLAB_OAUTH_AUTO_LOGIN,
|
|
78
|
+
openBrowser: env.GITLAB_OAUTH_OPEN_BROWSER
|
|
79
|
+
}),
|
|
73
80
|
defaultProject: env.GITLAB_DEFAULT_PROJECT,
|
|
74
81
|
autoResolveProjectFromGit: env.GITLAB_AUTO_RESOLVE_PROJECT_FROM_GIT,
|
|
75
82
|
autoDetectedProject
|
|
@@ -84,6 +91,66 @@ function loadConfig() {
|
|
|
84
91
|
}
|
|
85
92
|
};
|
|
86
93
|
}
|
|
94
|
+
function parseGroupOAuthConfigJson(raw, defaults) {
|
|
95
|
+
if (!raw) {
|
|
96
|
+
return {};
|
|
97
|
+
}
|
|
98
|
+
let parsed;
|
|
99
|
+
try {
|
|
100
|
+
parsed = JSON.parse(raw);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
throw new Error('GITLAB_GROUP_OAUTH_CONFIG_JSON must be valid JSON.');
|
|
104
|
+
}
|
|
105
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
106
|
+
throw new Error('GITLAB_GROUP_OAUTH_CONFIG_JSON must be an object map.');
|
|
107
|
+
}
|
|
108
|
+
const result = {};
|
|
109
|
+
for (const [groupKey, value] of Object.entries(parsed)) {
|
|
110
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const rec = value;
|
|
114
|
+
result[groupKey] = {
|
|
115
|
+
clientId: toStringOrUndefined(rec.clientId ?? rec.client_id),
|
|
116
|
+
clientSecret: toStringOrUndefined(rec.clientSecret ?? rec.client_secret),
|
|
117
|
+
redirectUri: toStringOrUndefined(rec.redirectUri ?? rec.redirect_uri ?? defaults.redirectUri),
|
|
118
|
+
scopes: parseScopes(rec.scopes, defaults.scopes),
|
|
119
|
+
tokenStorePath: toStringOrUndefined(rec.tokenStorePath ?? rec.token_store_path) ??
|
|
120
|
+
(0, node_path_1.join)((0, node_os_1.homedir)(), '.config', 'gitlab-mcp', `${sanitizeForFilename(groupKey)}-token.json`),
|
|
121
|
+
autoLogin: toBooleanOrDefault(rec.autoLogin ?? rec.auto_login, defaults.autoLogin),
|
|
122
|
+
openBrowser: toBooleanOrDefault(rec.openBrowser ?? rec.open_browser, defaults.openBrowser),
|
|
123
|
+
bootstrapAccessToken: toStringOrUndefined(rec.bootstrapAccessToken ?? rec.bootstrap_access_token)
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
128
|
+
function parseScopes(value, fallback) {
|
|
129
|
+
if (typeof value === 'string') {
|
|
130
|
+
return splitCsv(value);
|
|
131
|
+
}
|
|
132
|
+
if (Array.isArray(value)) {
|
|
133
|
+
return value
|
|
134
|
+
.map((item) => (typeof item === 'string' ? item.trim() : ''))
|
|
135
|
+
.filter((item) => item.length > 0);
|
|
136
|
+
}
|
|
137
|
+
return fallback;
|
|
138
|
+
}
|
|
139
|
+
function toStringOrUndefined(value) {
|
|
140
|
+
return typeof value === 'string' && value.trim() !== '' ? value : undefined;
|
|
141
|
+
}
|
|
142
|
+
function toBooleanOrDefault(value, fallback) {
|
|
143
|
+
if (typeof value === 'boolean') {
|
|
144
|
+
return value;
|
|
145
|
+
}
|
|
146
|
+
if (typeof value === 'string') {
|
|
147
|
+
return value !== 'false';
|
|
148
|
+
}
|
|
149
|
+
return fallback;
|
|
150
|
+
}
|
|
151
|
+
function sanitizeForFilename(value) {
|
|
152
|
+
return value.replace(/[^a-zA-Z0-9._-]+/g, '_');
|
|
153
|
+
}
|
|
87
154
|
function resolveAccessToken(env) {
|
|
88
155
|
if (env.GITLAB_AUTH_MODE === 'oauth') {
|
|
89
156
|
return env.GITLAB_OAUTH_ACCESS_TOKEN;
|
package/docs/USER_GUIDE.md
CHANGED
|
@@ -59,8 +59,9 @@ export GITLAB_OAUTH_SCOPES="api";
|
|
|
59
59
|
export GITLAB_OAUTH_TOKEN_STORE_PATH="$HOME/.config/gitlab-mcp/token.json";
|
|
60
60
|
export GITLAB_OAUTH_AUTO_LOGIN="true";
|
|
61
61
|
export GITLAB_OAUTH_OPEN_BROWSER="false";
|
|
62
|
+
export GITLAB_GROUP_OAUTH_CONFIG_JSON='{"konoha7":{"clientId":"<ID_1>","clientSecret":"<SECRET_1>","redirectUri":"http://127.0.0.1:8787/oauth/callback"},"othergroup":{"clientId":"<ID_2>","clientSecret":"<SECRET_2>","redirectUri":"http://127.0.0.1:8788/oauth/callback"}}';
|
|
62
63
|
|
|
63
|
-
# optional
|
|
64
|
+
# optional fallback if auto-detect from git remote is unavailable
|
|
64
65
|
export GITLAB_DEFAULT_PROJECT="group/repo";
|
|
65
66
|
export GITLAB_AUTO_RESOLVE_PROJECT_FROM_GIT="true";
|
|
66
67
|
|
|
@@ -70,6 +71,11 @@ npx -y gitlab-mcp-agent-server
|
|
|
70
71
|
|
|
71
72
|
После изменения `config.toml` перезапусти Codex.
|
|
72
73
|
|
|
74
|
+
Как выбирается OAuth-конфиг при `GITLAB_GROUP_OAUTH_CONFIG_JSON`:
|
|
75
|
+
1. Сервер берёт `resolved_project` (например `konoha7/subgroup/repo`).
|
|
76
|
+
2. Находит самый длинный совпадающий group key.
|
|
77
|
+
3. Использует соответствующие `clientId/clientSecret/redirectUri/tokenStorePath`.
|
|
78
|
+
|
|
73
79
|
## 4. Что происходит при первом запуске
|
|
74
80
|
|
|
75
81
|
1. Агент вызывает любой GitLab tool (например `gitlab_list_labels`).
|
|
@@ -97,7 +103,7 @@ npx -y gitlab-mcp-agent-server
|
|
|
97
103
|
2. Ограничь права файла:
|
|
98
104
|
- `chmod 600 /home/<user>/.config/gitlab-mcp/token.json`
|
|
99
105
|
3. Оставь `GITLAB_OAUTH_OPEN_BROWSER=false` для headless окружений.
|
|
100
|
-
4.
|
|
106
|
+
4. Для multi-repo режима лучше не задавать `GITLAB_DEFAULT_PROJECT`, чтобы проект брался из `git remote` текущего `cwd`.
|
|
101
107
|
|
|
102
108
|
## 7. Быстрая проверка работоспособности
|
|
103
109
|
|