@zereight/mcp-gitlab 2.1.14 โ 2.1.15
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/schemas.js
CHANGED
|
@@ -2580,7 +2580,7 @@ export const GitLabProjectMemberSchema = z.object({
|
|
|
2580
2580
|
});
|
|
2581
2581
|
// Markdown upload schemas
|
|
2582
2582
|
export const GitLabMarkdownUploadSchema = z.object({
|
|
2583
|
-
id: z.coerce.number(),
|
|
2583
|
+
id: z.preprocess((val) => (val == null ? undefined : val), z.coerce.number().optional()),
|
|
2584
2584
|
alt: z.string(),
|
|
2585
2585
|
url: z.string(),
|
|
2586
2586
|
full_path: z.string(),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env ts-node
|
|
2
|
-
import { GetFileContentsSchema, GitLabFileContentSchema, GitLabRepositorySchema, CreatePipelineSchema, CreateCommitStatusSchema, ListCommitStatusesSchema, CreateIssueNoteSchema, CreateMergeRequestEmojiReactionSchema, CreateIssueEmojiReactionSchema, DeleteMergeRequestEmojiReactionSchema, DeleteIssueEmojiReactionSchema, CreateWorkItemEmojiReactionSchema, CreateWorkItemNoteEmojiReactionSchema, CreateIssueSchema, ListIssuesSchema, ListMergeRequestsSchema, ListLabelsSchema, GitLabMergeRequestSchema, GitLabTreeItemSchema, GetMergeRequestSchema, ListMergeRequestPipelinesSchema, GetRepositoryTreeSchema, GitLabUserFullSchema } from '../schemas.js';
|
|
2
|
+
import { GetFileContentsSchema, GitLabFileContentSchema, GitLabRepositorySchema, CreatePipelineSchema, CreateCommitStatusSchema, ListCommitStatusesSchema, CreateIssueNoteSchema, CreateMergeRequestEmojiReactionSchema, CreateIssueEmojiReactionSchema, DeleteMergeRequestEmojiReactionSchema, DeleteIssueEmojiReactionSchema, CreateWorkItemEmojiReactionSchema, CreateWorkItemNoteEmojiReactionSchema, CreateIssueSchema, ListIssuesSchema, ListMergeRequestsSchema, ListLabelsSchema, GitLabMergeRequestSchema, GitLabTreeItemSchema, GetMergeRequestSchema, ListMergeRequestPipelinesSchema, GetRepositoryTreeSchema, GitLabUserFullSchema, GitLabMarkdownUploadSchema, } from '../schemas.js';
|
|
3
3
|
function runGetFileContentsSchemaTests() {
|
|
4
4
|
console.log('๐งช Testing GetFileContentsSchema...');
|
|
5
5
|
const cases = [
|
|
@@ -1122,6 +1122,75 @@ function runGetRepositoryTreeSchemaTests() {
|
|
|
1122
1122
|
console.log(`\nResults: ${passed} passed, ${failed} failed`);
|
|
1123
1123
|
return { passed, failed };
|
|
1124
1124
|
}
|
|
1125
|
+
function runGitLabMarkdownUploadSchemaTests() {
|
|
1126
|
+
console.log('\n=== GitLabMarkdownUpload Schema Tests ===');
|
|
1127
|
+
const idlessUpload = {
|
|
1128
|
+
alt: 'report.md',
|
|
1129
|
+
url: '/uploads/c617e74a47dfb1a6dd59d419619b725d/report.md',
|
|
1130
|
+
full_path: '/group/project/uploads/c617e74a47dfb1a6dd59d419619b725d/report.md',
|
|
1131
|
+
markdown: '[report.md](/uploads/c617e74a47dfb1a6dd59d419619b725d/report.md)',
|
|
1132
|
+
};
|
|
1133
|
+
const cases = [
|
|
1134
|
+
{
|
|
1135
|
+
name: 'schema:markdown_upload:accepts-idless-response',
|
|
1136
|
+
input: idlessUpload,
|
|
1137
|
+
expectedId: 'absent',
|
|
1138
|
+
},
|
|
1139
|
+
{
|
|
1140
|
+
name: 'schema:markdown_upload:accepts-numeric-id',
|
|
1141
|
+
input: { ...idlessUpload, id: 42 },
|
|
1142
|
+
expectedId: 42,
|
|
1143
|
+
},
|
|
1144
|
+
{
|
|
1145
|
+
name: 'schema:markdown_upload:coerces-string-id',
|
|
1146
|
+
input: { ...idlessUpload, id: '99' },
|
|
1147
|
+
expectedId: 99,
|
|
1148
|
+
},
|
|
1149
|
+
{
|
|
1150
|
+
name: 'schema:markdown_upload:treats-null-id-as-absent',
|
|
1151
|
+
input: { ...idlessUpload, id: null },
|
|
1152
|
+
expectedId: 'absent',
|
|
1153
|
+
},
|
|
1154
|
+
{
|
|
1155
|
+
name: 'schema:markdown_upload:rejects-invalid-id',
|
|
1156
|
+
input: { ...idlessUpload, id: 'not-a-number' },
|
|
1157
|
+
shouldFail: true,
|
|
1158
|
+
},
|
|
1159
|
+
];
|
|
1160
|
+
let passed = 0;
|
|
1161
|
+
let failed = 0;
|
|
1162
|
+
cases.forEach(testCase => {
|
|
1163
|
+
const result = { name: testCase.name, status: 'failed' };
|
|
1164
|
+
const parsed = GitLabMarkdownUploadSchema.safeParse(testCase.input);
|
|
1165
|
+
if (testCase.shouldFail) {
|
|
1166
|
+
result.status = parsed.success ? 'failed' : 'passed';
|
|
1167
|
+
if (parsed.success)
|
|
1168
|
+
result.error = 'Expected schema validation to fail';
|
|
1169
|
+
}
|
|
1170
|
+
else if (!parsed.success) {
|
|
1171
|
+
result.error = parsed.error?.message || 'Schema validation failed';
|
|
1172
|
+
}
|
|
1173
|
+
else if (testCase.expectedId === 'absent' && parsed.data.id !== undefined) {
|
|
1174
|
+
result.error = `Expected id undefined, got ${parsed.data.id}`;
|
|
1175
|
+
}
|
|
1176
|
+
else if (typeof testCase.expectedId === 'number' && parsed.data.id !== testCase.expectedId) {
|
|
1177
|
+
result.error = `Expected id ${testCase.expectedId}, got ${parsed.data.id}`;
|
|
1178
|
+
}
|
|
1179
|
+
else {
|
|
1180
|
+
result.status = 'passed';
|
|
1181
|
+
}
|
|
1182
|
+
if (result.status === 'passed') {
|
|
1183
|
+
passed++;
|
|
1184
|
+
console.log(`โ
${result.name}`);
|
|
1185
|
+
}
|
|
1186
|
+
else {
|
|
1187
|
+
failed++;
|
|
1188
|
+
console.log(`โ ${result.name}: ${result.error}`);
|
|
1189
|
+
}
|
|
1190
|
+
});
|
|
1191
|
+
console.log(`\nResults: ${passed} passed, ${failed} failed`);
|
|
1192
|
+
return { passed, failed };
|
|
1193
|
+
}
|
|
1125
1194
|
function runGitLabUserFullSchemaTests() {
|
|
1126
1195
|
console.log('๐งช Testing GitLabUserFullSchema...');
|
|
1127
1196
|
const adminResponse = {
|
|
@@ -1219,8 +1288,9 @@ if (import.meta.url === `file://${process.argv[1]}`) {
|
|
|
1219
1288
|
const treeItemResult = runGitLabTreeItemSchemaTests();
|
|
1220
1289
|
const repositoryTreeResult = runGetRepositoryTreeSchemaTests();
|
|
1221
1290
|
const gitLabUserFullResult = runGitLabUserFullSchemaTests();
|
|
1222
|
-
const
|
|
1223
|
-
const
|
|
1291
|
+
const gitLabMarkdownUploadResult = runGitLabMarkdownUploadSchemaTests();
|
|
1292
|
+
const totalPassed = getFileContentsResult.passed + fileContentResult.passed + createPipelineResult.passed + commitStatusResult.passed + createIssueNoteResult.passed + getMergeRequestResult.passed + listMergeRequestPipelinesResult.passed + gitLabMergeRequestResult.passed + emojiReactionResult.passed + repositorySchemaResult.passed + labelsCoercionResult.passed + listLabelsResult.passed + treeItemResult.passed + repositoryTreeResult.passed + gitLabUserFullResult.passed + gitLabMarkdownUploadResult.passed;
|
|
1293
|
+
const totalFailed = getFileContentsResult.failed + fileContentResult.failed + createPipelineResult.failed + commitStatusResult.failed + createIssueNoteResult.failed + getMergeRequestResult.failed + listMergeRequestPipelinesResult.failed + gitLabMergeRequestResult.failed + emojiReactionResult.failed + repositorySchemaResult.failed + labelsCoercionResult.failed + listLabelsResult.failed + treeItemResult.failed + repositoryTreeResult.failed + gitLabUserFullResult.failed + gitLabMarkdownUploadResult.failed;
|
|
1224
1294
|
console.log(`\nTotal Results: ${totalPassed} passed, ${totalFailed} failed`);
|
|
1225
1295
|
if (totalFailed > 0) {
|
|
1226
1296
|
process.exit(1);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, test, before, after } from 'node:test';
|
|
1
|
+
import { describe, test, before, after, beforeEach } from 'node:test';
|
|
2
2
|
import assert from 'node:assert';
|
|
3
3
|
import { spawn } from 'node:child_process';
|
|
4
4
|
import fs from 'node:fs';
|
|
@@ -55,12 +55,18 @@ const MOCK_UPLOAD_RESPONSE = {
|
|
|
55
55
|
full_path: '/test-group/test-project/uploads/abc123secret/test-file.txt',
|
|
56
56
|
markdown: '[test-file.txt](/uploads/abc123secret/test-file.txt)',
|
|
57
57
|
};
|
|
58
|
+
let uploadResponse = MOCK_UPLOAD_RESPONSE;
|
|
58
59
|
describe('upload_markdown', () => {
|
|
59
60
|
let mockGitLab;
|
|
60
61
|
let env;
|
|
61
62
|
// Captured per-request state, reset before each invocation via the handler
|
|
62
63
|
let lastContentType;
|
|
63
64
|
let lastRawBody;
|
|
65
|
+
beforeEach(() => {
|
|
66
|
+
uploadResponse = MOCK_UPLOAD_RESPONSE;
|
|
67
|
+
lastContentType = undefined;
|
|
68
|
+
lastRawBody = undefined;
|
|
69
|
+
});
|
|
64
70
|
before(async () => {
|
|
65
71
|
const port = await findMockServerPort(9200);
|
|
66
72
|
mockGitLab = new MockGitLabServer({ port, validTokens: [MOCK_TOKEN] });
|
|
@@ -75,7 +81,7 @@ describe('upload_markdown', () => {
|
|
|
75
81
|
req.on('data', (chunk) => chunks.push(chunk));
|
|
76
82
|
req.on('end', () => {
|
|
77
83
|
lastRawBody = Buffer.concat(chunks).toString('binary');
|
|
78
|
-
res.status(201).json(
|
|
84
|
+
res.status(201).json(uploadResponse);
|
|
79
85
|
});
|
|
80
86
|
});
|
|
81
87
|
});
|
|
@@ -139,6 +145,27 @@ describe('upload_markdown', () => {
|
|
|
139
145
|
fs.unlinkSync(tmpFile);
|
|
140
146
|
}
|
|
141
147
|
});
|
|
148
|
+
test('accepts upload responses without id from older self-hosted GitLab', async () => {
|
|
149
|
+
const { id: _id, ...idlessUploadResponse } = MOCK_UPLOAD_RESPONSE;
|
|
150
|
+
uploadResponse = idlessUploadResponse;
|
|
151
|
+
const tmpFile = path.join(os.tmpdir(), 'mcp-upload-idless-response-test.txt');
|
|
152
|
+
fs.writeFileSync(tmpFile, 'idless response field test');
|
|
153
|
+
try {
|
|
154
|
+
const raw = await callUploadMarkdown({ project_id: TEST_PROJECT_ID, file_path: tmpFile }, env);
|
|
155
|
+
assert.ok(!raw.error, `Unexpected RPC error: ${raw.error?.message}`);
|
|
156
|
+
const text = raw.result?.content?.[0]?.text;
|
|
157
|
+
assert.ok(text, 'Result should contain a text content block');
|
|
158
|
+
const parsed = JSON.parse(text);
|
|
159
|
+
assert.strictEqual(parsed.id, undefined);
|
|
160
|
+
assert.strictEqual(parsed.markdown, MOCK_UPLOAD_RESPONSE.markdown);
|
|
161
|
+
assert.strictEqual(parsed.url, MOCK_UPLOAD_RESPONSE.url);
|
|
162
|
+
assert.strictEqual(parsed.alt, MOCK_UPLOAD_RESPONSE.alt);
|
|
163
|
+
assert.strictEqual(parsed.full_path, MOCK_UPLOAD_RESPONSE.full_path);
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
fs.unlinkSync(tmpFile);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
142
169
|
test('returns an error when the file does not exist', async () => {
|
|
143
170
|
const raw = await callUploadMarkdown({ project_id: TEST_PROJECT_ID, file_path: '/nonexistent/no-such-file.txt' }, env);
|
|
144
171
|
const hasError = typeof raw.error?.message === 'string' ||
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zereight/mcp-gitlab",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.15",
|
|
4
4
|
"mcpName": "io.github.zereight/gitlab-mcp",
|
|
5
5
|
"description": "GitLab MCP server for projects, merge requests, issues, pipelines, wiki, releases, and more",
|
|
6
6
|
"keywords": [
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"changelog": "auto-changelog -p",
|
|
52
52
|
"test": "npm run test:all",
|
|
53
53
|
"test:all": "npm run build && npm run test:mock && npm run test:live",
|
|
54
|
-
"test:mock": "node --import tsx/esm --test test/remote-auth-simple-test.ts && node --import tsx/esm --test test/mcp-oauth-tests.ts && node --import tsx/esm --test test/streamable-http-static-token-auth.test.ts && tsx test/oauth-tests.ts && tsx test/test-list-merge-requests.ts && node --import tsx/esm --test test/test-merge-request-pipelines.ts && tsx test/test-list-project-members.ts && tsx test/test-download-attachment.ts && node --import tsx/esm --test test/test-job-artifacts.ts && node --import tsx/esm --test test/test-deployment-tools.ts && node --import tsx/esm --test test/test-merge-request-approval-state-tools.ts && node --import tsx/esm --test test/test-search-code.ts && node --import tsx/esm --test test/test-tags.ts && node --import tsx/esm --test test/test-toolset-filtering.ts && node --import tsx/esm --test test/test-ci-lint.ts && node --import tsx/esm --test test/test-todos.ts && node --import tsx/esm --test test/test-auth-retry.ts && node --import tsx/esm --test test/test-issue-description-patch.ts && node --import tsx/esm --test test/test-geteffectiveprojectid.ts && node --import tsx/esm --test test/test-get-file-blame.ts && node --import tsx/esm --test test/stateless/codec.test.ts test/stateless/client-id.test.ts test/stateless/callback-proxy.test.ts test/stateless/session-id.test.ts test/stateless/session-id-integration.test.ts test/stateless/config-ttl.test.ts",
|
|
54
|
+
"test:mock": "node --import tsx/esm --test test/remote-auth-simple-test.ts && node --import tsx/esm --test test/mcp-oauth-tests.ts && node --import tsx/esm --test test/streamable-http-static-token-auth.test.ts && tsx test/oauth-tests.ts && tsx test/test-list-merge-requests.ts && node --import tsx/esm --test test/test-merge-request-pipelines.ts && tsx test/test-list-project-members.ts && tsx test/test-download-attachment.ts && node --import tsx/esm --test test/test-upload-markdown.ts && node --import tsx/esm --test test/test-job-artifacts.ts && node --import tsx/esm --test test/test-deployment-tools.ts && node --import tsx/esm --test test/test-merge-request-approval-state-tools.ts && node --import tsx/esm --test test/test-search-code.ts && node --import tsx/esm --test test/test-tags.ts && node --import tsx/esm --test test/test-toolset-filtering.ts && node --import tsx/esm --test test/test-ci-lint.ts && node --import tsx/esm --test test/test-todos.ts && node --import tsx/esm --test test/test-auth-retry.ts && node --import tsx/esm --test test/test-issue-description-patch.ts && node --import tsx/esm --test test/test-geteffectiveprojectid.ts && node --import tsx/esm --test test/test-get-file-blame.ts && node --import tsx/esm --test test/stateless/codec.test.ts test/stateless/client-id.test.ts test/stateless/callback-proxy.test.ts test/stateless/session-id.test.ts test/stateless/session-id-integration.test.ts test/stateless/config-ttl.test.ts",
|
|
55
55
|
"test:stateless": "npm run build && node --import tsx/esm --test test/stateless/codec.test.ts test/stateless/client-id.test.ts test/stateless/callback-proxy.test.ts test/stateless/session-id.test.ts test/stateless/session-id-integration.test.ts test/stateless/config-ttl.test.ts",
|
|
56
56
|
"test:mcp-oauth": "npm run build && node --import tsx/esm --test test/mcp-oauth-tests.ts",
|
|
57
57
|
"test:live": "node test/validate-api.js",
|