gitlab-mcp-agent-server 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/LICENSE +21 -0
- package/README.md +45 -0
- package/bin/gitlab-mcp-server.js +3 -0
- package/build/src/application/policies/issue-workflow-policy.js +38 -0
- package/build/src/application/services/project-resolver.js +23 -0
- package/build/src/application/use-cases/close-issue.js +13 -0
- package/build/src/application/use-cases/create-issue.js +13 -0
- package/build/src/application/use-cases/ensure-labels.js +34 -0
- package/build/src/application/use-cases/get-issue.js +13 -0
- package/build/src/application/use-cases/health-check.js +12 -0
- package/build/src/application/use-cases/list-issues.js +13 -0
- package/build/src/application/use-cases/list-labels.js +13 -0
- package/build/src/application/use-cases/list-projects.js +13 -0
- package/build/src/application/use-cases/update-issue-labels.js +47 -0
- package/build/src/domain/ports/gitlab-api.js +2 -0
- package/build/src/index.js +14 -0
- package/build/src/infrastructure/auth/gitlab-oauth-manager.js +210 -0
- package/build/src/infrastructure/auth/oauth-token-store.js +26 -0
- package/build/src/infrastructure/auth/token-provider.js +16 -0
- package/build/src/infrastructure/gitlab/gitlab-api-client.js +150 -0
- package/build/src/interface/mcp/create-mcp-server.js +59 -0
- package/build/src/interface/mcp/register-tools.js +199 -0
- package/build/src/shared/config.js +125 -0
- package/build/src/shared/errors.js +17 -0
- package/docs/README.md +16 -0
- package/docs/USER_GUIDE.md +120 -0
- package/package.json +53 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GitLabApiClient = void 0;
|
|
4
|
+
const errors_1 = require("../../shared/errors");
|
|
5
|
+
class GitLabApiClient {
|
|
6
|
+
options;
|
|
7
|
+
constructor(options) {
|
|
8
|
+
this.options = options;
|
|
9
|
+
}
|
|
10
|
+
async listProjects() {
|
|
11
|
+
const data = await this.request('/projects?simple=true&membership=true');
|
|
12
|
+
return data.map(mapProject);
|
|
13
|
+
}
|
|
14
|
+
async createIssue(input) {
|
|
15
|
+
const projectPath = encodeProjectRef(input.project);
|
|
16
|
+
const body = {
|
|
17
|
+
title: input.title,
|
|
18
|
+
description: input.description,
|
|
19
|
+
labels: input.labels?.join(','),
|
|
20
|
+
assignee_ids: input.assigneeIds
|
|
21
|
+
};
|
|
22
|
+
const data = await this.request(`/projects/${projectPath}/issues`, {
|
|
23
|
+
method: 'POST',
|
|
24
|
+
body: JSON.stringify(body)
|
|
25
|
+
});
|
|
26
|
+
return mapIssue(data);
|
|
27
|
+
}
|
|
28
|
+
async getIssue(input) {
|
|
29
|
+
const projectPath = encodeProjectRef(input.project);
|
|
30
|
+
const data = await this.request(`/projects/${projectPath}/issues/${input.issueIid}`);
|
|
31
|
+
return mapIssue(data);
|
|
32
|
+
}
|
|
33
|
+
async closeIssue(input) {
|
|
34
|
+
const projectPath = encodeProjectRef(input.project);
|
|
35
|
+
const data = await this.request(`/projects/${projectPath}/issues/${input.issueIid}`, {
|
|
36
|
+
method: 'PUT',
|
|
37
|
+
body: JSON.stringify({ state_event: 'close' })
|
|
38
|
+
});
|
|
39
|
+
return mapIssue(data);
|
|
40
|
+
}
|
|
41
|
+
async updateIssueLabels(input) {
|
|
42
|
+
const projectPath = encodeProjectRef(input.project);
|
|
43
|
+
const data = await this.request(`/projects/${projectPath}/issues/${input.issueIid}`, {
|
|
44
|
+
method: 'PUT',
|
|
45
|
+
body: JSON.stringify({ labels: input.labels.join(',') })
|
|
46
|
+
});
|
|
47
|
+
return mapIssue(data);
|
|
48
|
+
}
|
|
49
|
+
async listLabels(input) {
|
|
50
|
+
const projectPath = encodeProjectRef(input.project);
|
|
51
|
+
const query = input.search
|
|
52
|
+
? `/projects/${projectPath}/labels?search=${encodeURIComponent(input.search)}`
|
|
53
|
+
: `/projects/${projectPath}/labels`;
|
|
54
|
+
const data = await this.request(query);
|
|
55
|
+
return data.map(mapLabel);
|
|
56
|
+
}
|
|
57
|
+
async listIssues(input) {
|
|
58
|
+
const projectPath = encodeProjectRef(input.project);
|
|
59
|
+
const query = new URLSearchParams();
|
|
60
|
+
if (input.state) {
|
|
61
|
+
query.set('state', input.state);
|
|
62
|
+
}
|
|
63
|
+
if (input.search) {
|
|
64
|
+
query.set('search', input.search);
|
|
65
|
+
}
|
|
66
|
+
if (input.labels && input.labels.length > 0) {
|
|
67
|
+
query.set('labels', input.labels.join(','));
|
|
68
|
+
}
|
|
69
|
+
if (input.perPage) {
|
|
70
|
+
query.set('per_page', String(input.perPage));
|
|
71
|
+
}
|
|
72
|
+
if (input.page) {
|
|
73
|
+
query.set('page', String(input.page));
|
|
74
|
+
}
|
|
75
|
+
const querySuffix = query.size > 0 ? `?${query.toString()}` : '';
|
|
76
|
+
const data = await this.request(`/projects/${projectPath}/issues${querySuffix}`);
|
|
77
|
+
return data.map(mapIssue);
|
|
78
|
+
}
|
|
79
|
+
async createLabel(input) {
|
|
80
|
+
const projectPath = encodeProjectRef(input.project);
|
|
81
|
+
const data = await this.request(`/projects/${projectPath}/labels`, {
|
|
82
|
+
method: 'POST',
|
|
83
|
+
body: JSON.stringify({
|
|
84
|
+
name: input.name,
|
|
85
|
+
color: input.color ?? '#1f75cb',
|
|
86
|
+
description: input.description
|
|
87
|
+
})
|
|
88
|
+
});
|
|
89
|
+
return mapLabel(data);
|
|
90
|
+
}
|
|
91
|
+
async request(path, init) {
|
|
92
|
+
const token = await this.options.tokenProvider.getAccessToken();
|
|
93
|
+
if (!token) {
|
|
94
|
+
throw new errors_1.ConfigurationError('GitLab access token is not configured. Set GITLAB_OAUTH_ACCESS_TOKEN (oauth) or GITLAB_PAT (pat).');
|
|
95
|
+
}
|
|
96
|
+
const response = await fetch(`${this.options.apiUrl}${path}`, {
|
|
97
|
+
...init,
|
|
98
|
+
headers: {
|
|
99
|
+
Authorization: `Bearer ${token}`,
|
|
100
|
+
'Content-Type': 'application/json',
|
|
101
|
+
...(init?.headers ?? {})
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
if (!response.ok) {
|
|
105
|
+
const body = await safeReadBody(response);
|
|
106
|
+
throw new Error(`GitLab API ${response.status}: ${body}`);
|
|
107
|
+
}
|
|
108
|
+
return (await response.json());
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
exports.GitLabApiClient = GitLabApiClient;
|
|
112
|
+
function mapProject(project) {
|
|
113
|
+
return {
|
|
114
|
+
id: project.id,
|
|
115
|
+
name: project.name,
|
|
116
|
+
pathWithNamespace: project.path_with_namespace
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function mapIssue(issue) {
|
|
120
|
+
return {
|
|
121
|
+
id: issue.id,
|
|
122
|
+
iid: issue.iid,
|
|
123
|
+
projectId: issue.project_id,
|
|
124
|
+
title: issue.title,
|
|
125
|
+
description: issue.description,
|
|
126
|
+
state: issue.state,
|
|
127
|
+
labels: issue.labels,
|
|
128
|
+
webUrl: issue.web_url,
|
|
129
|
+
updatedAt: issue.updated_at,
|
|
130
|
+
closedAt: issue.closed_at
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function mapLabel(label) {
|
|
134
|
+
return {
|
|
135
|
+
name: label.name,
|
|
136
|
+
color: label.color,
|
|
137
|
+
description: label.description
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function encodeProjectRef(project) {
|
|
141
|
+
return encodeURIComponent(String(project));
|
|
142
|
+
}
|
|
143
|
+
async function safeReadBody(response) {
|
|
144
|
+
try {
|
|
145
|
+
return await response.text();
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return 'Unable to read response body';
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createMcpServer = createMcpServer;
|
|
4
|
+
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
5
|
+
const issue_workflow_policy_1 = require("../../application/policies/issue-workflow-policy");
|
|
6
|
+
const project_resolver_1 = require("../../application/services/project-resolver");
|
|
7
|
+
const close_issue_1 = require("../../application/use-cases/close-issue");
|
|
8
|
+
const create_issue_1 = require("../../application/use-cases/create-issue");
|
|
9
|
+
const ensure_labels_1 = require("../../application/use-cases/ensure-labels");
|
|
10
|
+
const get_issue_1 = require("../../application/use-cases/get-issue");
|
|
11
|
+
const health_check_1 = require("../../application/use-cases/health-check");
|
|
12
|
+
const list_labels_1 = require("../../application/use-cases/list-labels");
|
|
13
|
+
const list_issues_1 = require("../../application/use-cases/list-issues");
|
|
14
|
+
const update_issue_labels_1 = require("../../application/use-cases/update-issue-labels");
|
|
15
|
+
const gitlab_oauth_manager_1 = require("../../infrastructure/auth/gitlab-oauth-manager");
|
|
16
|
+
const token_provider_1 = require("../../infrastructure/auth/token-provider");
|
|
17
|
+
const gitlab_api_client_1 = require("../../infrastructure/gitlab/gitlab-api-client");
|
|
18
|
+
const config_1 = require("../../shared/config");
|
|
19
|
+
const register_tools_1 = require("./register-tools");
|
|
20
|
+
function createMcpServer() {
|
|
21
|
+
const config = (0, config_1.loadConfig)();
|
|
22
|
+
const tokenProvider = config.gitlab.authMode === 'oauth'
|
|
23
|
+
? new gitlab_oauth_manager_1.GitLabOAuthManager({
|
|
24
|
+
apiUrl: config.gitlab.apiUrl,
|
|
25
|
+
clientId: config.gitlab.oauth.clientId,
|
|
26
|
+
clientSecret: config.gitlab.oauth.clientSecret,
|
|
27
|
+
redirectUri: config.gitlab.oauth.redirectUri,
|
|
28
|
+
scopes: config.gitlab.oauth.scopes,
|
|
29
|
+
bootstrapAccessToken: config.gitlab.accessToken,
|
|
30
|
+
tokenStorePath: config.gitlab.oauth.tokenStorePath,
|
|
31
|
+
autoLogin: config.gitlab.oauth.autoLogin,
|
|
32
|
+
openBrowser: config.gitlab.oauth.openBrowser
|
|
33
|
+
})
|
|
34
|
+
: new token_provider_1.StaticTokenProvider(config.gitlab.accessToken);
|
|
35
|
+
const gitlabApiClient = new gitlab_api_client_1.GitLabApiClient({
|
|
36
|
+
apiUrl: config.gitlab.apiUrl,
|
|
37
|
+
tokenProvider
|
|
38
|
+
});
|
|
39
|
+
const server = new mcp_js_1.McpServer({
|
|
40
|
+
name: 'gitlab-mcp-server',
|
|
41
|
+
version: '0.1.0'
|
|
42
|
+
});
|
|
43
|
+
const projectResolver = new project_resolver_1.ProjectResolver(config);
|
|
44
|
+
const issueWorkflowPolicy = new issue_workflow_policy_1.IssueWorkflowPolicy(config);
|
|
45
|
+
(0, register_tools_1.registerTools)(server, {
|
|
46
|
+
config,
|
|
47
|
+
projectResolver,
|
|
48
|
+
issueWorkflowPolicy,
|
|
49
|
+
healthCheckUseCase: new health_check_1.HealthCheckUseCase(),
|
|
50
|
+
createIssueUseCase: new create_issue_1.CreateIssueUseCase(gitlabApiClient),
|
|
51
|
+
getIssueUseCase: new get_issue_1.GetIssueUseCase(gitlabApiClient),
|
|
52
|
+
closeIssueUseCase: new close_issue_1.CloseIssueUseCase(gitlabApiClient),
|
|
53
|
+
updateIssueLabelsUseCase: new update_issue_labels_1.UpdateIssueLabelsUseCase(gitlabApiClient),
|
|
54
|
+
listIssuesUseCase: new list_issues_1.ListIssuesUseCase(gitlabApiClient),
|
|
55
|
+
listLabelsUseCase: new list_labels_1.ListLabelsUseCase(gitlabApiClient),
|
|
56
|
+
ensureLabelsUseCase: new ensure_labels_1.EnsureLabelsUseCase(gitlabApiClient)
|
|
57
|
+
});
|
|
58
|
+
return server;
|
|
59
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerTools = registerTools;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
const errors_1 = require("../../shared/errors");
|
|
6
|
+
function registerTools(server, deps) {
|
|
7
|
+
server.registerTool('health_check', {
|
|
8
|
+
title: 'Health Check',
|
|
9
|
+
description: 'Returns server status.',
|
|
10
|
+
inputSchema: {
|
|
11
|
+
ping: zod_1.z.string().optional()
|
|
12
|
+
}
|
|
13
|
+
}, async ({ ping }) => {
|
|
14
|
+
return runTool(async () => deps.healthCheckUseCase.execute({ ping }));
|
|
15
|
+
});
|
|
16
|
+
server.registerTool('gitlab_create_issue', {
|
|
17
|
+
title: 'GitLab Create Issue',
|
|
18
|
+
description: 'Create an issue in a GitLab project.',
|
|
19
|
+
inputSchema: {
|
|
20
|
+
project: zod_1.z.union([zod_1.z.string(), zod_1.z.number()]).optional(),
|
|
21
|
+
title: zod_1.z.string().min(1),
|
|
22
|
+
description: zod_1.z.string().optional(),
|
|
23
|
+
labels: zod_1.z.array(zod_1.z.string()).optional(),
|
|
24
|
+
assignee_ids: zod_1.z.array(zod_1.z.number()).optional()
|
|
25
|
+
}
|
|
26
|
+
}, async ({ project, title, description, labels, assignee_ids }) => {
|
|
27
|
+
return runTool(async () => {
|
|
28
|
+
deps.issueWorkflowPolicy.assertActionAllowed('create');
|
|
29
|
+
deps.issueWorkflowPolicy.assertLabelsAllowed(labels ?? []);
|
|
30
|
+
const resolvedProject = deps.projectResolver.resolveProject(project);
|
|
31
|
+
const issue = await deps.createIssueUseCase.execute({
|
|
32
|
+
project: resolvedProject,
|
|
33
|
+
title,
|
|
34
|
+
description,
|
|
35
|
+
labels,
|
|
36
|
+
assigneeIds: assignee_ids
|
|
37
|
+
});
|
|
38
|
+
return { resolved_project: resolvedProject, issue };
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
server.registerTool('gitlab_get_issue', {
|
|
42
|
+
title: 'GitLab Get Issue',
|
|
43
|
+
description: 'Get issue details by issue IID.',
|
|
44
|
+
inputSchema: {
|
|
45
|
+
project: zod_1.z.union([zod_1.z.string(), zod_1.z.number()]).optional(),
|
|
46
|
+
issue_iid: zod_1.z.number().int().positive()
|
|
47
|
+
}
|
|
48
|
+
}, async ({ project, issue_iid }) => {
|
|
49
|
+
return runTool(async () => {
|
|
50
|
+
deps.issueWorkflowPolicy.assertEnabled();
|
|
51
|
+
const resolvedProject = deps.projectResolver.resolveProject(project);
|
|
52
|
+
const issue = await deps.getIssueUseCase.execute({
|
|
53
|
+
project: resolvedProject,
|
|
54
|
+
issueIid: issue_iid
|
|
55
|
+
});
|
|
56
|
+
return { resolved_project: resolvedProject, issue };
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
server.registerTool('gitlab_close_issue', {
|
|
60
|
+
title: 'GitLab Close Issue',
|
|
61
|
+
description: 'Close an issue by issue IID.',
|
|
62
|
+
inputSchema: {
|
|
63
|
+
project: zod_1.z.union([zod_1.z.string(), zod_1.z.number()]).optional(),
|
|
64
|
+
issue_iid: zod_1.z.number().int().positive()
|
|
65
|
+
}
|
|
66
|
+
}, async ({ project, issue_iid }) => {
|
|
67
|
+
return runTool(async () => {
|
|
68
|
+
deps.issueWorkflowPolicy.assertActionAllowed('close');
|
|
69
|
+
const resolvedProject = deps.projectResolver.resolveProject(project);
|
|
70
|
+
const issue = await deps.closeIssueUseCase.execute({
|
|
71
|
+
project: resolvedProject,
|
|
72
|
+
issueIid: issue_iid
|
|
73
|
+
});
|
|
74
|
+
return { resolved_project: resolvedProject, issue };
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
server.registerTool('gitlab_update_issue_labels', {
|
|
78
|
+
title: 'GitLab Update Issue Labels',
|
|
79
|
+
description: 'Update labels on an issue.',
|
|
80
|
+
inputSchema: {
|
|
81
|
+
project: zod_1.z.union([zod_1.z.string(), zod_1.z.number()]).optional(),
|
|
82
|
+
issue_iid: zod_1.z.number().int().positive(),
|
|
83
|
+
mode: zod_1.z.enum(['replace', 'add', 'remove']),
|
|
84
|
+
labels: zod_1.z.array(zod_1.z.string()).min(1)
|
|
85
|
+
}
|
|
86
|
+
}, async ({ project, issue_iid, mode, labels }) => {
|
|
87
|
+
return runTool(async () => {
|
|
88
|
+
deps.issueWorkflowPolicy.assertActionAllowed('label_update');
|
|
89
|
+
deps.issueWorkflowPolicy.assertLabelsAllowed(labels);
|
|
90
|
+
const resolvedProject = deps.projectResolver.resolveProject(project);
|
|
91
|
+
const stateLabels = deps.config.issueWorkflow.allowedLabels;
|
|
92
|
+
const issue = await deps.updateIssueLabelsUseCase.execute({
|
|
93
|
+
project: resolvedProject,
|
|
94
|
+
issueIid: issue_iid,
|
|
95
|
+
mode,
|
|
96
|
+
labels,
|
|
97
|
+
autoRemovePreviousStateLabels: deps.config.issueWorkflow.autoRemovePreviousStateLabels,
|
|
98
|
+
stateLabels
|
|
99
|
+
});
|
|
100
|
+
return { resolved_project: resolvedProject, issue };
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
server.registerTool('gitlab_list_issues', {
|
|
104
|
+
title: 'GitLab List Issues',
|
|
105
|
+
description: 'List issues in a project.',
|
|
106
|
+
inputSchema: {
|
|
107
|
+
project: zod_1.z.union([zod_1.z.string(), zod_1.z.number()]).optional(),
|
|
108
|
+
state: zod_1.z.enum(['opened', 'closed', 'all']).optional(),
|
|
109
|
+
search: zod_1.z.string().optional(),
|
|
110
|
+
labels: zod_1.z.array(zod_1.z.string()).optional(),
|
|
111
|
+
per_page: zod_1.z.number().int().min(1).max(100).optional(),
|
|
112
|
+
page: zod_1.z.number().int().min(1).optional()
|
|
113
|
+
}
|
|
114
|
+
}, async ({ project, state, search, labels, per_page, page }) => {
|
|
115
|
+
return runTool(async () => {
|
|
116
|
+
deps.issueWorkflowPolicy.assertEnabled();
|
|
117
|
+
if (labels) {
|
|
118
|
+
deps.issueWorkflowPolicy.assertLabelsAllowed(labels);
|
|
119
|
+
}
|
|
120
|
+
const resolvedProject = deps.projectResolver.resolveProject(project);
|
|
121
|
+
const issues = await deps.listIssuesUseCase.execute({
|
|
122
|
+
project: resolvedProject,
|
|
123
|
+
state,
|
|
124
|
+
search,
|
|
125
|
+
labels,
|
|
126
|
+
perPage: per_page,
|
|
127
|
+
page
|
|
128
|
+
});
|
|
129
|
+
return { resolved_project: resolvedProject, issues };
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
server.registerTool('gitlab_list_labels', {
|
|
133
|
+
title: 'GitLab List Labels',
|
|
134
|
+
description: 'List labels for a project.',
|
|
135
|
+
inputSchema: {
|
|
136
|
+
project: zod_1.z.union([zod_1.z.string(), zod_1.z.number()]).optional(),
|
|
137
|
+
search: zod_1.z.string().optional()
|
|
138
|
+
}
|
|
139
|
+
}, async ({ project, search }) => {
|
|
140
|
+
return runTool(async () => {
|
|
141
|
+
deps.issueWorkflowPolicy.assertEnabled();
|
|
142
|
+
const resolvedProject = deps.projectResolver.resolveProject(project);
|
|
143
|
+
const labels = await deps.listLabelsUseCase.execute({
|
|
144
|
+
project: resolvedProject,
|
|
145
|
+
search
|
|
146
|
+
});
|
|
147
|
+
return { resolved_project: resolvedProject, labels };
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
server.registerTool('gitlab_ensure_labels', {
|
|
151
|
+
title: 'GitLab Ensure Labels',
|
|
152
|
+
description: 'Create missing labels in a project.',
|
|
153
|
+
inputSchema: {
|
|
154
|
+
project: zod_1.z.union([zod_1.z.string(), zod_1.z.number()]).optional(),
|
|
155
|
+
labels: zod_1.z
|
|
156
|
+
.array(zod_1.z.object({
|
|
157
|
+
name: zod_1.z.string().min(1),
|
|
158
|
+
color: zod_1.z.string().optional(),
|
|
159
|
+
description: zod_1.z.string().optional()
|
|
160
|
+
}))
|
|
161
|
+
.min(1)
|
|
162
|
+
}
|
|
163
|
+
}, async ({ project, labels }) => {
|
|
164
|
+
return runTool(async () => {
|
|
165
|
+
deps.issueWorkflowPolicy.assertActionAllowed('label_update');
|
|
166
|
+
deps.issueWorkflowPolicy.assertLabelsAllowed(labels.map((label) => label.name));
|
|
167
|
+
const resolvedProject = deps.projectResolver.resolveProject(project);
|
|
168
|
+
const result = await deps.ensureLabelsUseCase.execute({
|
|
169
|
+
project: resolvedProject,
|
|
170
|
+
labels
|
|
171
|
+
});
|
|
172
|
+
return { resolved_project: resolvedProject, ...result };
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
async function runTool(execute) {
|
|
177
|
+
try {
|
|
178
|
+
const data = await execute();
|
|
179
|
+
return asJsonResult({ ok: true, data });
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
const message = error instanceof errors_1.PolicyError
|
|
183
|
+
? error.message
|
|
184
|
+
: error instanceof Error
|
|
185
|
+
? error.message
|
|
186
|
+
: 'Unexpected tool error';
|
|
187
|
+
return asJsonResult({ ok: false, error: message });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function asJsonResult(payload) {
|
|
191
|
+
return {
|
|
192
|
+
content: [
|
|
193
|
+
{
|
|
194
|
+
type: 'text',
|
|
195
|
+
text: JSON.stringify(payload)
|
|
196
|
+
}
|
|
197
|
+
]
|
|
198
|
+
};
|
|
199
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.loadConfig = loadConfig;
|
|
4
|
+
const node_child_process_1 = require("node:child_process");
|
|
5
|
+
const node_os_1 = require("node:os");
|
|
6
|
+
const node_path_1 = require("node:path");
|
|
7
|
+
const zod_1 = require("zod");
|
|
8
|
+
const DEFAULT_TOKEN_STORE_PATH = (0, node_path_1.join)((0, node_os_1.homedir)(), '.config', 'gitlab-mcp', 'token.json');
|
|
9
|
+
const EnvSchema = zod_1.z.object({
|
|
10
|
+
GITLAB_API_URL: zod_1.z.string().url().default('https://gitlab.com/api/v4'),
|
|
11
|
+
GITLAB_AUTH_MODE: zod_1.z.enum(['oauth', 'pat']).default('oauth'),
|
|
12
|
+
GITLAB_OAUTH_ACCESS_TOKEN: zod_1.z.string().optional(),
|
|
13
|
+
GITLAB_OAUTH_CLIENT_ID: zod_1.z.string().optional(),
|
|
14
|
+
GITLAB_OAUTH_CLIENT_SECRET: zod_1.z.string().optional(),
|
|
15
|
+
GITLAB_OAUTH_REDIRECT_URI: zod_1.z.string().optional(),
|
|
16
|
+
GITLAB_OAUTH_SCOPES: zod_1.z.string().default('api'),
|
|
17
|
+
GITLAB_OAUTH_TOKEN_STORE_PATH: zod_1.z.string().default(DEFAULT_TOKEN_STORE_PATH),
|
|
18
|
+
GITLAB_OAUTH_AUTO_LOGIN: zod_1.z
|
|
19
|
+
.enum(['true', 'false'])
|
|
20
|
+
.optional()
|
|
21
|
+
.transform((value) => value !== 'false'),
|
|
22
|
+
GITLAB_OAUTH_OPEN_BROWSER: zod_1.z
|
|
23
|
+
.enum(['true', 'false'])
|
|
24
|
+
.optional()
|
|
25
|
+
.transform((value) => value !== 'false'),
|
|
26
|
+
GITLAB_PAT: zod_1.z.string().optional(),
|
|
27
|
+
GITLAB_DEFAULT_PROJECT: zod_1.z.string().optional(),
|
|
28
|
+
GITLAB_AUTO_RESOLVE_PROJECT_FROM_GIT: zod_1.z
|
|
29
|
+
.enum(['true', 'false'])
|
|
30
|
+
.optional()
|
|
31
|
+
.transform((value) => value !== 'false'),
|
|
32
|
+
ISSUE_WORKFLOW_ENABLED: zod_1.z
|
|
33
|
+
.enum(['true', 'false'])
|
|
34
|
+
.optional()
|
|
35
|
+
.transform((value) => value !== 'false'),
|
|
36
|
+
ISSUE_WORKFLOW_ALLOW_CREATE: zod_1.z
|
|
37
|
+
.enum(['true', 'false'])
|
|
38
|
+
.optional()
|
|
39
|
+
.transform((value) => value !== 'false'),
|
|
40
|
+
ISSUE_WORKFLOW_ALLOW_CLOSE: zod_1.z
|
|
41
|
+
.enum(['true', 'false'])
|
|
42
|
+
.optional()
|
|
43
|
+
.transform((value) => value !== 'false'),
|
|
44
|
+
ISSUE_WORKFLOW_ALLOW_LABEL_UPDATE: zod_1.z
|
|
45
|
+
.enum(['true', 'false'])
|
|
46
|
+
.optional()
|
|
47
|
+
.transform((value) => value !== 'false'),
|
|
48
|
+
ISSUE_WORKFLOW_ALLOWED_LABELS: zod_1.z.string().optional(),
|
|
49
|
+
ISSUE_WORKFLOW_AUTO_REMOVE_PREVIOUS_STATE_LABELS: zod_1.z
|
|
50
|
+
.enum(['true', 'false'])
|
|
51
|
+
.optional()
|
|
52
|
+
.transform((value) => value !== 'false')
|
|
53
|
+
});
|
|
54
|
+
function loadConfig() {
|
|
55
|
+
const env = EnvSchema.parse(process.env);
|
|
56
|
+
const autoDetectedProject = env.GITLAB_AUTO_RESOLVE_PROJECT_FROM_GIT
|
|
57
|
+
? detectProjectFromGitRemote()
|
|
58
|
+
: undefined;
|
|
59
|
+
return {
|
|
60
|
+
gitlab: {
|
|
61
|
+
apiUrl: env.GITLAB_API_URL,
|
|
62
|
+
authMode: env.GITLAB_AUTH_MODE,
|
|
63
|
+
accessToken: resolveAccessToken(env),
|
|
64
|
+
oauth: {
|
|
65
|
+
clientId: env.GITLAB_OAUTH_CLIENT_ID,
|
|
66
|
+
clientSecret: env.GITLAB_OAUTH_CLIENT_SECRET,
|
|
67
|
+
redirectUri: env.GITLAB_OAUTH_REDIRECT_URI,
|
|
68
|
+
scopes: splitCsv(env.GITLAB_OAUTH_SCOPES),
|
|
69
|
+
tokenStorePath: env.GITLAB_OAUTH_TOKEN_STORE_PATH,
|
|
70
|
+
autoLogin: env.GITLAB_OAUTH_AUTO_LOGIN,
|
|
71
|
+
openBrowser: env.GITLAB_OAUTH_OPEN_BROWSER
|
|
72
|
+
},
|
|
73
|
+
defaultProject: env.GITLAB_DEFAULT_PROJECT,
|
|
74
|
+
autoResolveProjectFromGit: env.GITLAB_AUTO_RESOLVE_PROJECT_FROM_GIT,
|
|
75
|
+
autoDetectedProject
|
|
76
|
+
},
|
|
77
|
+
issueWorkflow: {
|
|
78
|
+
enabled: env.ISSUE_WORKFLOW_ENABLED,
|
|
79
|
+
allowCreate: env.ISSUE_WORKFLOW_ALLOW_CREATE,
|
|
80
|
+
allowClose: env.ISSUE_WORKFLOW_ALLOW_CLOSE,
|
|
81
|
+
allowLabelUpdate: env.ISSUE_WORKFLOW_ALLOW_LABEL_UPDATE,
|
|
82
|
+
allowedLabels: splitCsv(env.ISSUE_WORKFLOW_ALLOWED_LABELS),
|
|
83
|
+
autoRemovePreviousStateLabels: env.ISSUE_WORKFLOW_AUTO_REMOVE_PREVIOUS_STATE_LABELS
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function resolveAccessToken(env) {
|
|
88
|
+
if (env.GITLAB_AUTH_MODE === 'oauth') {
|
|
89
|
+
return env.GITLAB_OAUTH_ACCESS_TOKEN;
|
|
90
|
+
}
|
|
91
|
+
return env.GITLAB_PAT;
|
|
92
|
+
}
|
|
93
|
+
function splitCsv(value) {
|
|
94
|
+
if (!value) {
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
return value
|
|
98
|
+
.split(',')
|
|
99
|
+
.map((entry) => entry.trim())
|
|
100
|
+
.filter((entry) => entry.length > 0);
|
|
101
|
+
}
|
|
102
|
+
function detectProjectFromGitRemote() {
|
|
103
|
+
try {
|
|
104
|
+
const origin = (0, node_child_process_1.execSync)('git config --get remote.origin.url', {
|
|
105
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
106
|
+
})
|
|
107
|
+
.toString()
|
|
108
|
+
.trim();
|
|
109
|
+
return parseProjectPathFromRemote(origin);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function parseProjectPathFromRemote(remote) {
|
|
116
|
+
const sshMatch = remote.match(/^[^@]+@[^:]+:(.+?)(?:\.git)?$/);
|
|
117
|
+
if (sshMatch?.[1]) {
|
|
118
|
+
return sshMatch[1];
|
|
119
|
+
}
|
|
120
|
+
const httpsMatch = remote.match(/^https?:\/\/[^/]+\/(.+?)(?:\.git)?$/);
|
|
121
|
+
if (httpsMatch?.[1]) {
|
|
122
|
+
return httpsMatch[1];
|
|
123
|
+
}
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ConfigurationError = exports.PolicyError = void 0;
|
|
4
|
+
class PolicyError extends Error {
|
|
5
|
+
constructor(message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = 'PolicyError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
exports.PolicyError = PolicyError;
|
|
11
|
+
class ConfigurationError extends Error {
|
|
12
|
+
constructor(message) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = 'ConfigurationError';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
exports.ConfigurationError = ConfigurationError;
|
package/docs/README.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Docs
|
|
2
|
+
|
|
3
|
+
Документация разделена на два блока:
|
|
4
|
+
|
|
5
|
+
- `docs/agent/` — настройки ИИ-агента (роли, правила, запреты, commit policy).
|
|
6
|
+
- `docs/architecture/` — архитектурные решения и структура модулей.
|
|
7
|
+
- `docs/tasks/` — задачи для агента и статус выполнения.
|
|
8
|
+
- `docs/agent/context7-policy.md` — регламент использования актуальной документации.
|
|
9
|
+
- `docs/agent/stack-decision.md` — выбранный стек и решение по framework.
|
|
10
|
+
- `docs/architecture/issue-tools-v0.md` — контракт MCP tools для issue/labels workflow.
|
|
11
|
+
- `docs/architecture/auth-strategy.md` — стратегия аутентификации (OAuth-first).
|
|
12
|
+
- `docs/USER_GUIDE.md` — инструкция для конечного пользователя.
|
|
13
|
+
- `docs/GITHUB_DESCRIPTION.md` — тексты для оформления GitHub-репозитория.
|
|
14
|
+
- `docs/RELEASE_CHECKLIST.md` — чеклист первого релиза (GitHub + npm).
|
|
15
|
+
|
|
16
|
+
Точка входа для агента: `AGENTS.md` в корне репозитория.
|