@zereight/mcp-gitlab 2.1.70 → 2.2.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/README.ko.md +3 -1
- package/README.md +6 -2
- package/README.pt-BR.md +1 -1
- package/README.zh-CN.md +3 -1
- package/build/config.js +12 -4
- package/build/deprecated-env.js +47 -0
- package/build/index.js +369 -184
- package/build/oauth-device-flow.js +80 -0
- package/build/oauth.js +135 -18
- package/build/schemas.js +1 -1
- package/build/server/remote-http-stack.js +39 -0
- package/build/tools/discover-activation.js +38 -0
- package/build/tools/exposed-tools.js +35 -0
- package/build/tools/listed-tools.js +76 -0
- package/build/tools/registry.js +226 -57
- package/build/tools/tool-profile.js +66 -0
- package/build/utils/compact-tool-allowlist.js +14 -0
- package/build/utils/logger.js +34 -12
- package/package.json +3 -3
- package/build/scripts/check-skill-sync.js +0 -143
- package/build/scripts/generate-server-card.js +0 -39
- package/build/scripts/generate-tool-coverage.js +0 -58
- package/build/scripts/generate-tool-docs.js +0 -430
- package/build/scripts/tool-coverage/coverage.js +0 -341
- package/build/scripts/tool-coverage/coverage.test.js +0 -142
- package/build/test/callback-proxy-tests.js +0 -321
- package/build/test/cli/args.test.js +0 -175
- package/build/test/cli/boolean-flags.test.js +0 -106
- package/build/test/cli/compact-mcp-result.test.js +0 -93
- package/build/test/cli/credentials.test.js +0 -153
- package/build/test/cli/exposure.test.js +0 -104
- package/build/test/cli/load-config-snapshot.js +0 -5
- package/build/test/cli/output.test.js +0 -74
- package/build/test/cli/replay-command.test.js +0 -37
- package/build/test/cli/router.test.js +0 -183
- package/build/test/client-pool-test.js +0 -109
- package/build/test/clients/client.js +0 -34
- package/build/test/clients/custom-header-client.js +0 -122
- package/build/test/clients/sse-client.js +0 -95
- package/build/test/clients/stdio-client.js +0 -113
- package/build/test/clients/streamable-http-client.js +0 -95
- package/build/test/config-allowed-groups.test.js +0 -97
- package/build/test/create-commit-actions.test.js +0 -102
- package/build/test/draft-note-schema.test.js +0 -44
- package/build/test/dynamic-api-url-allowlist.test.js +0 -121
- package/build/test/dynamic-api-url-test.js +0 -304
- package/build/test/dynamic-routing-tests.js +0 -495
- package/build/test/get-merge-request-discussion-schema.test.js +0 -30
- package/build/test/gitlab-api-tool-errors.test.js +0 -112
- package/build/test/gitlab-artifact-entry-schema.test.js +0 -44
- package/build/test/gitlab-diff-schema.test.js +0 -36
- package/build/test/gitlab-pipeline-schedule-schema.test.js +0 -32
- package/build/test/gitlab-tag-schema.test.js +0 -51
- package/build/test/group-milestone-schema.test.js +0 -45
- package/build/test/mcp-oauth-tests.js +0 -905
- package/build/test/mcp-server-name.test.js +0 -87
- package/build/test/merge-merge-request-schema.test.js +0 -75
- package/build/test/multi-server-test.js +0 -190
- package/build/test/no-proxy-integration-test.js +0 -183
- package/build/test/no-proxy-test.js +0 -149
- package/build/test/nullable-gitlab-response-fields.test.js +0 -37
- package/build/test/nullish-tool-arguments-schema.test.js +0 -118
- package/build/test/oauth-device-flow-tests.js +0 -450
- package/build/test/oauth-download-url.test.js +0 -157
- package/build/test/oauth-startup.test.js +0 -204
- package/build/test/oauth-tests.js +0 -428
- package/build/test/path-segment-encoding.test.js +0 -98
- package/build/test/remote-auth-simple-test.js +0 -277
- package/build/test/remote-auth-tests.js +0 -314
- package/build/test/response-masking.test.js +0 -445
- package/build/test/schema-tests.js +0 -1679
- package/build/test/server/metrics.test.js +0 -47
- package/build/test/server/request-helpers.test.js +0 -48
- package/build/test/server/version.test.js +0 -13
- package/build/test/sse-auth-guard.test.js +0 -96
- package/build/test/sse-session-limits.test.js +0 -273
- package/build/test/stateless/callback-proxy.test.js +0 -510
- package/build/test/stateless/client-id.test.js +0 -177
- package/build/test/stateless/codec.test.js +0 -328
- package/build/test/stateless/config-ttl.test.js +0 -149
- package/build/test/stateless/consumed-proxy-code-cache.test.js +0 -150
- package/build/test/stateless/session-id-integration.test.js +0 -675
- package/build/test/stateless/session-id.test.js +0 -157
- package/build/test/streamable-http-concurrent-session.test.js +0 -171
- package/build/test/streamable-http-dns-rebinding.test.js +0 -185
- package/build/test/streamable-http-sse-stream.test.js +0 -350
- package/build/test/streamable-http-static-token-auth.test.js +0 -157
- package/build/test/streamable-http-unauthenticated-discovery.test.js +0 -207
- package/build/test/test-all-transport-server.js +0 -255
- package/build/test/test-auth-retry.js +0 -195
- package/build/test/test-blank-filters.js +0 -156
- package/build/test/test-ci-catalog.js +0 -177
- package/build/test/test-ci-lint.js +0 -191
- package/build/test/test-ci-variables.js +0 -309
- package/build/test/test-create-repository.js +0 -120
- package/build/test/test-dependency-proxy.js +0 -195
- package/build/test/test-deployment-tools.js +0 -463
- package/build/test/test-download-attachment.js +0 -190
- package/build/test/test-dynamic-project-scope.js +0 -524
- package/build/test/test-empty-toolsets.js +0 -535
- package/build/test/test-get-file-blame.js +0 -145
- package/build/test/test-get-file-contents.js +0 -128
- package/build/test/test-geteffectiveprojectid.js +0 -804
- package/build/test/test-health-check.js +0 -115
- package/build/test/test-issue-description-patch.js +0 -256
- package/build/test/test-job-artifacts.js +0 -275
- package/build/test/test-json-schema.js +0 -166
- package/build/test/test-list-group-members.js +0 -150
- package/build/test/test-list-group-merge-requests.js +0 -165
- package/build/test/test-list-issues.js +0 -170
- package/build/test/test-list-merge-requests.js +0 -201
- package/build/test/test-list-milestones.js +0 -145
- package/build/test/test-list-project-members.js +0 -132
- package/build/test/test-merge-request-approval-state-tools.js +0 -213
- package/build/test/test-merge-request-approvals.js +0 -187
- package/build/test/test-merge-request-pipelines.js +0 -106
- package/build/test/test-mr-diffs-filter.js +0 -132
- package/build/test/test-mr-file-diffs.js +0 -251
- package/build/test/test-oauth-proxy-rate-limit.js +0 -133
- package/build/test/test-orbit.js +0 -155
- package/build/test/test-permission-mode.js +0 -424
- package/build/test/test-pipeline-schedules.js +0 -484
- package/build/test/test-protected-branches.js +0 -174
- package/build/test/test-remote-downloads.js +0 -510
- package/build/test/test-search-code.js +0 -275
- package/build/test/test-snippets.js +0 -735
- package/build/test/test-tags.js +0 -220
- package/build/test/test-todos.js +0 -195
- package/build/test/test-token-optimizations.js +0 -691
- package/build/test/test-toolset-filtering.js +0 -499
- package/build/test/test-update-issue-slim.js +0 -141
- package/build/test/test-update-project.js +0 -112
- package/build/test/test-upload-markdown.js +0 -206
- package/build/test/test-vulnerabilities.js +0 -399
- package/build/test/test-webhooks.js +0 -218
- package/build/test/tool-description-quality.test.js +0 -50
- package/build/test/utils/bulk-publish-options.test.js +0 -32
- package/build/test/utils/download-token.test.js +0 -35
- package/build/test/utils/forwarded-public-base-url.test.js +0 -46
- package/build/test/utils/gitlab-instance-version.test.js +0 -32
- package/build/test/utils/gitlab-version-gate.test.js +0 -74
- package/build/test/utils/graphql-query.test.js +0 -168
- package/build/test/utils/helpers-path.test.js +0 -104
- package/build/test/utils/jmespath-tool-result.test.js +0 -91
- package/build/test/utils/masking.test.js +0 -384
- package/build/test/utils/merge-request-position.test.js +0 -57
- package/build/test/utils/mock-gitlab-server.js +0 -614
- package/build/test/utils/proxy-client-ip.test.js +0 -28
- package/build/test/utils/redact-sensitive.test.js +0 -32
- package/build/test/utils/safe-redirect-fetch.test.js +0 -463
- package/build/test/utils/server-launcher.js +0 -231
- package/build/test/utils/tool-args.test.js +0 -210
- package/build/test/utils/url.test.js +0 -141
- package/build/test/utils/version-check.test.js +0 -239
- package/build/test/utils/wiki-title.test.js +0 -21
- package/build/test-note.js +0 -54
- package/build/test-resolve-issue-note.js +0 -127
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import { describe, test } from "node:test";
|
|
2
|
-
import assert from "node:assert";
|
|
3
|
-
import { spawn } from "child_process";
|
|
4
|
-
import { MockGitLabServer, findMockServerPort } from "./utils/mock-gitlab-server.js";
|
|
5
|
-
const MOCK_TOKEN = "mock-update-project-token";
|
|
6
|
-
const TEST_PROJECT_ID = "123";
|
|
7
|
-
async function callUpdateProject(args, env) {
|
|
8
|
-
return new Promise((resolve, reject) => {
|
|
9
|
-
const proc = spawn("node", ["build/index.js"], {
|
|
10
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
11
|
-
env: { ...process.env, ...env },
|
|
12
|
-
});
|
|
13
|
-
let output = "";
|
|
14
|
-
let errorOutput = "";
|
|
15
|
-
proc.stdout?.on("data", (d) => (output += d));
|
|
16
|
-
proc.stderr?.on("data", (d) => (errorOutput += d));
|
|
17
|
-
proc.on("close", code => {
|
|
18
|
-
if (code !== 0) {
|
|
19
|
-
reject(new Error(`Process exited with code ${code}: ${errorOutput}`));
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
const line = output.split("\n").find(l => l.startsWith("{"));
|
|
23
|
-
if (!line) {
|
|
24
|
-
reject(new Error("No JSON output found"));
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
const response = JSON.parse(line);
|
|
28
|
-
if (response.error) {
|
|
29
|
-
reject(new Error(response.error?.message ?? String(response.error)));
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
const content = response.result?.content?.[0]?.text;
|
|
33
|
-
resolve(content ? JSON.parse(content) : response.result);
|
|
34
|
-
});
|
|
35
|
-
proc.stdin?.end(JSON.stringify({
|
|
36
|
-
jsonrpc: "2.0",
|
|
37
|
-
id: 1,
|
|
38
|
-
method: "tools/call",
|
|
39
|
-
params: { name: "update_project", arguments: args },
|
|
40
|
-
}) + "\n");
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
describe("update_project", () => {
|
|
44
|
-
test("sends project settings to PUT /projects/:id", async () => {
|
|
45
|
-
const mockPort = await findMockServerPort();
|
|
46
|
-
const mockServer = new MockGitLabServer({ port: mockPort, validTokens: [MOCK_TOKEN] });
|
|
47
|
-
let receivedBody;
|
|
48
|
-
mockServer.addMockHandler("put", `/projects/${TEST_PROJECT_ID}`, (req, res) => {
|
|
49
|
-
receivedBody = req.body;
|
|
50
|
-
res.json({ id: Number(TEST_PROJECT_ID), ...receivedBody });
|
|
51
|
-
});
|
|
52
|
-
await mockServer.start();
|
|
53
|
-
try {
|
|
54
|
-
const result = await callUpdateProject({
|
|
55
|
-
project_id: TEST_PROJECT_ID,
|
|
56
|
-
description: "Managed by MCP",
|
|
57
|
-
visibility: "private",
|
|
58
|
-
topics: ["ai", "mcp"],
|
|
59
|
-
issues_access_level: "enabled",
|
|
60
|
-
wiki_access_level: "disabled",
|
|
61
|
-
remove_source_branch_after_merge: "true",
|
|
62
|
-
}, {
|
|
63
|
-
GITLAB_API_URL: `${mockServer.getUrl()}/api/v4`,
|
|
64
|
-
GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN,
|
|
65
|
-
});
|
|
66
|
-
assert.deepStrictEqual(receivedBody, {
|
|
67
|
-
description: "Managed by MCP",
|
|
68
|
-
visibility: "private",
|
|
69
|
-
topics: ["ai", "mcp"],
|
|
70
|
-
issues_access_level: "enabled",
|
|
71
|
-
wiki_access_level: "disabled",
|
|
72
|
-
remove_source_branch_after_merge: true,
|
|
73
|
-
});
|
|
74
|
-
assert.strictEqual(result.description, "Managed by MCP");
|
|
75
|
-
}
|
|
76
|
-
finally {
|
|
77
|
-
await mockServer.stop();
|
|
78
|
-
}
|
|
79
|
-
});
|
|
80
|
-
test("keeps string false as boolean false", async () => {
|
|
81
|
-
const mockPort = await findMockServerPort();
|
|
82
|
-
const mockServer = new MockGitLabServer({ port: mockPort, validTokens: [MOCK_TOKEN] });
|
|
83
|
-
let receivedBody;
|
|
84
|
-
mockServer.addMockHandler("put", `/projects/${TEST_PROJECT_ID}`, (req, res) => {
|
|
85
|
-
receivedBody = req.body;
|
|
86
|
-
res.json({ id: Number(TEST_PROJECT_ID), ...receivedBody });
|
|
87
|
-
});
|
|
88
|
-
await mockServer.start();
|
|
89
|
-
try {
|
|
90
|
-
await callUpdateProject({ project_id: TEST_PROJECT_ID, remove_source_branch_after_merge: "false" }, {
|
|
91
|
-
GITLAB_API_URL: `${mockServer.getUrl()}/api/v4`,
|
|
92
|
-
GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN,
|
|
93
|
-
});
|
|
94
|
-
assert.strictEqual(receivedBody?.remove_source_branch_after_merge, false);
|
|
95
|
-
}
|
|
96
|
-
finally {
|
|
97
|
-
await mockServer.stop();
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
|
-
test("rejects public access level for non-pages features", async () => {
|
|
101
|
-
await assert.rejects(() => callUpdateProject({ project_id: TEST_PROJECT_ID, issues_access_level: "public" }, {
|
|
102
|
-
GITLAB_API_URL: "https://gitlab.example.com/api/v4",
|
|
103
|
-
GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN,
|
|
104
|
-
}), /(Invalid enum value|Invalid option)/);
|
|
105
|
-
});
|
|
106
|
-
test("rejects empty updates before calling GitLab", async () => {
|
|
107
|
-
await assert.rejects(() => callUpdateProject({ project_id: TEST_PROJECT_ID }, {
|
|
108
|
-
GITLAB_API_URL: "https://gitlab.example.com/api/v4",
|
|
109
|
-
GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN,
|
|
110
|
-
}), /Provide at least one project setting/);
|
|
111
|
-
});
|
|
112
|
-
});
|
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
import { describe, test, before, after, beforeEach } from 'node:test';
|
|
2
|
-
import assert from 'node:assert';
|
|
3
|
-
import { spawn } from 'node:child_process';
|
|
4
|
-
import fs from 'node:fs';
|
|
5
|
-
import os from 'node:os';
|
|
6
|
-
import path from 'node:path';
|
|
7
|
-
import { MockGitLabServer, findMockServerPort } from './utils/mock-gitlab-server.js';
|
|
8
|
-
const MOCK_TOKEN = 'glpat-mock-token-12345';
|
|
9
|
-
const TEST_PROJECT_ID = '123';
|
|
10
|
-
function callUploadMarkdown(args, env, timeoutMs = 15_000) {
|
|
11
|
-
return new Promise((resolve, reject) => {
|
|
12
|
-
const proc = spawn('node', ['build/index.js'], {
|
|
13
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
14
|
-
env: { ...process.env, ...env },
|
|
15
|
-
});
|
|
16
|
-
const timer = setTimeout(() => {
|
|
17
|
-
proc.kill();
|
|
18
|
-
reject(new Error(`Process timed out after ${timeoutMs}ms`));
|
|
19
|
-
}, timeoutMs);
|
|
20
|
-
let stdout = '';
|
|
21
|
-
let stderr = '';
|
|
22
|
-
proc.stdout?.on('data', (d) => (stdout += d.toString()));
|
|
23
|
-
proc.stderr?.on('data', (d) => (stderr += d.toString()));
|
|
24
|
-
proc.on('error', (err) => {
|
|
25
|
-
clearTimeout(timer);
|
|
26
|
-
reject(new Error(`Failed to spawn process: ${err.message}`));
|
|
27
|
-
});
|
|
28
|
-
proc.on('close', () => {
|
|
29
|
-
clearTimeout(timer);
|
|
30
|
-
const lines = stdout.split('\n').filter(l => l.trim().startsWith('{'));
|
|
31
|
-
for (const line of lines) {
|
|
32
|
-
try {
|
|
33
|
-
const parsed = JSON.parse(line);
|
|
34
|
-
if (parsed.id === 1) {
|
|
35
|
-
resolve(parsed);
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
catch { /* try next line */ }
|
|
40
|
-
}
|
|
41
|
-
reject(new Error(`No matching JSON-RPC response found.\nstdout: ${stdout}\nstderr: ${stderr}`));
|
|
42
|
-
});
|
|
43
|
-
proc.stdin?.end(JSON.stringify({
|
|
44
|
-
jsonrpc: '2.0',
|
|
45
|
-
id: 1,
|
|
46
|
-
method: 'tools/call',
|
|
47
|
-
params: { name: 'upload_markdown', arguments: args },
|
|
48
|
-
}) + '\n');
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
const MOCK_UPLOAD_RESPONSE = {
|
|
52
|
-
id: 42,
|
|
53
|
-
alt: 'test-file.txt',
|
|
54
|
-
url: '/uploads/abc123secret/test-file.txt',
|
|
55
|
-
full_path: '/test-group/test-project/uploads/abc123secret/test-file.txt',
|
|
56
|
-
markdown: '[test-file.txt](/uploads/abc123secret/test-file.txt)',
|
|
57
|
-
};
|
|
58
|
-
let uploadResponse = MOCK_UPLOAD_RESPONSE;
|
|
59
|
-
describe('upload_markdown', () => {
|
|
60
|
-
let mockGitLab;
|
|
61
|
-
let env;
|
|
62
|
-
// Captured per-request state, reset before each invocation via the handler
|
|
63
|
-
let lastContentType;
|
|
64
|
-
let lastRawBody;
|
|
65
|
-
beforeEach(() => {
|
|
66
|
-
uploadResponse = MOCK_UPLOAD_RESPONSE;
|
|
67
|
-
lastContentType = undefined;
|
|
68
|
-
lastRawBody = undefined;
|
|
69
|
-
});
|
|
70
|
-
before(async () => {
|
|
71
|
-
const port = await findMockServerPort();
|
|
72
|
-
mockGitLab = new MockGitLabServer({ port, validTokens: [MOCK_TOKEN] });
|
|
73
|
-
await mockGitLab.start();
|
|
74
|
-
env = {
|
|
75
|
-
GITLAB_API_URL: `${mockGitLab.getUrl()}/api/v4`,
|
|
76
|
-
GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN,
|
|
77
|
-
};
|
|
78
|
-
mockGitLab.addMockHandler('post', `/projects/${TEST_PROJECT_ID}/uploads`, (req, res) => {
|
|
79
|
-
lastContentType = req.headers['content-type'];
|
|
80
|
-
const chunks = [];
|
|
81
|
-
req.on('data', (chunk) => chunks.push(chunk));
|
|
82
|
-
req.on('end', () => {
|
|
83
|
-
lastRawBody = Buffer.concat(chunks).toString('binary');
|
|
84
|
-
res.status(201).json(uploadResponse);
|
|
85
|
-
});
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
after(async () => {
|
|
89
|
-
await mockGitLab.stop();
|
|
90
|
-
});
|
|
91
|
-
test('Content-Type is multipart/form-data with a boundary', async () => {
|
|
92
|
-
const tmpFile = `mcp-upload-ct-test-${process.pid}.txt`;
|
|
93
|
-
fs.writeFileSync(tmpFile, 'content-type test');
|
|
94
|
-
try {
|
|
95
|
-
await callUploadMarkdown({ project_id: TEST_PROJECT_ID, file_path: tmpFile }, env);
|
|
96
|
-
assert.ok(lastContentType, 'Content-Type header must be present');
|
|
97
|
-
assert.ok(lastContentType.startsWith('multipart/form-data'), `Expected multipart/form-data, got: ${lastContentType}`);
|
|
98
|
-
assert.ok(lastContentType.includes('boundary='), `Content-Type must include boundary, got: ${lastContentType}`);
|
|
99
|
-
}
|
|
100
|
-
finally {
|
|
101
|
-
fs.unlinkSync(tmpFile);
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
|
-
test('multipart body contains a "file" field with the file content', async () => {
|
|
105
|
-
const tmpFile = `mcp-upload-body-test-${process.pid}.txt`;
|
|
106
|
-
const fileContent = 'hello from multipart upload test';
|
|
107
|
-
fs.writeFileSync(tmpFile, fileContent);
|
|
108
|
-
try {
|
|
109
|
-
await callUploadMarkdown({ project_id: TEST_PROJECT_ID, file_path: tmpFile }, env);
|
|
110
|
-
assert.ok(lastRawBody, 'Request body must be captured');
|
|
111
|
-
assert.ok(lastRawBody.includes('name="file"'), 'Multipart body should include a field named "file"');
|
|
112
|
-
assert.ok(lastRawBody.includes(fileContent), 'Multipart body should contain the uploaded file content');
|
|
113
|
-
}
|
|
114
|
-
finally {
|
|
115
|
-
fs.unlinkSync(tmpFile);
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
test('multipart body includes the original filename', async () => {
|
|
119
|
-
const tmpFile = `mcp-upload-filename-check-${process.pid}.txt`;
|
|
120
|
-
fs.writeFileSync(tmpFile, 'filename check');
|
|
121
|
-
try {
|
|
122
|
-
await callUploadMarkdown({ project_id: TEST_PROJECT_ID, file_path: tmpFile }, env);
|
|
123
|
-
assert.ok(lastRawBody, 'Request body must be captured');
|
|
124
|
-
assert.ok(lastRawBody.includes(`mcp-upload-filename-check-${process.pid}.txt`), 'Multipart body should include the original filename');
|
|
125
|
-
}
|
|
126
|
-
finally {
|
|
127
|
-
fs.unlinkSync(tmpFile);
|
|
128
|
-
}
|
|
129
|
-
});
|
|
130
|
-
test('returns markdown, url, alt, and full_path from upload response', async () => {
|
|
131
|
-
const tmpFile = `mcp-upload-response-test-${process.pid}.txt`;
|
|
132
|
-
fs.writeFileSync(tmpFile, 'response field test');
|
|
133
|
-
try {
|
|
134
|
-
const raw = await callUploadMarkdown({ project_id: TEST_PROJECT_ID, file_path: tmpFile }, env);
|
|
135
|
-
assert.ok(!raw.error, `Unexpected RPC error: ${raw.error?.message}`);
|
|
136
|
-
const text = raw.result?.content?.[0]?.text;
|
|
137
|
-
assert.ok(text, 'Result should contain a text content block');
|
|
138
|
-
const parsed = JSON.parse(text);
|
|
139
|
-
assert.strictEqual(parsed.markdown, MOCK_UPLOAD_RESPONSE.markdown);
|
|
140
|
-
assert.strictEqual(parsed.url, MOCK_UPLOAD_RESPONSE.url);
|
|
141
|
-
assert.strictEqual(parsed.alt, MOCK_UPLOAD_RESPONSE.alt);
|
|
142
|
-
assert.strictEqual(parsed.full_path, MOCK_UPLOAD_RESPONSE.full_path);
|
|
143
|
-
}
|
|
144
|
-
finally {
|
|
145
|
-
fs.unlinkSync(tmpFile);
|
|
146
|
-
}
|
|
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 = `mcp-upload-idless-response-test-${process.pid}.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
|
-
});
|
|
169
|
-
test('returns an error when the file does not exist', async () => {
|
|
170
|
-
const raw = await callUploadMarkdown({ project_id: TEST_PROJECT_ID, file_path: `mcp-upload-missing-${process.pid}.txt` }, env);
|
|
171
|
-
const hasError = typeof raw.error?.message === 'string' ||
|
|
172
|
-
raw.result?.content?.some(c => c.text && (c.text.toLowerCase().includes('not found') || c.text.toLowerCase().includes('error')));
|
|
173
|
-
assert.ok(hasError, 'Should return an error for a nonexistent file path');
|
|
174
|
-
});
|
|
175
|
-
test('rejects absolute file_path (path traversal / arbitrary read)', async () => {
|
|
176
|
-
const raw = await callUploadMarkdown({ project_id: TEST_PROJECT_ID, file_path: '/etc/passwd' }, env);
|
|
177
|
-
const msg = (typeof raw.error?.message === 'string' ? raw.error.message : '') +
|
|
178
|
-
(raw.result?.content?.map(c => c.text || '').join(' ') || '');
|
|
179
|
-
assert.ok(msg.toLowerCase().includes('traversal') || msg.toLowerCase().includes('invalid'), `Expected traversal rejection, got: ${msg}`);
|
|
180
|
-
});
|
|
181
|
-
test('rejects file_path with ".." traversal', async () => {
|
|
182
|
-
const raw = await callUploadMarkdown({ project_id: TEST_PROJECT_ID, file_path: '../../../etc/passwd' }, env);
|
|
183
|
-
const msg = (typeof raw.error?.message === 'string' ? raw.error.message : '') +
|
|
184
|
-
(raw.result?.content?.map(c => c.text || '').join(' ') || '');
|
|
185
|
-
assert.ok(msg.toLowerCase().includes('traversal') || msg.toLowerCase().includes('invalid'), `Expected traversal rejection, got: ${msg}`);
|
|
186
|
-
});
|
|
187
|
-
test('rejects file_path that is a symlink escape', async () => {
|
|
188
|
-
const linkName = `mcp-upload-symlink-${process.pid}.txt`;
|
|
189
|
-
const target = path.join(os.tmpdir(), `mcp-upload-secret-${process.pid}.txt`);
|
|
190
|
-
fs.writeFileSync(target, 'secret-content');
|
|
191
|
-
fs.symlinkSync(target, linkName);
|
|
192
|
-
try {
|
|
193
|
-
const raw = await callUploadMarkdown({ project_id: TEST_PROJECT_ID, file_path: linkName }, env);
|
|
194
|
-
const msg = (typeof raw.error?.message === 'string' ? raw.error.message : '') +
|
|
195
|
-
(raw.result?.content?.map(c => c.text || '').join(' ') || '');
|
|
196
|
-
assert.ok(msg.toLowerCase().includes('symbolic') ||
|
|
197
|
-
msg.toLowerCase().includes('symlink') ||
|
|
198
|
-
msg.toLowerCase().includes('escapes') ||
|
|
199
|
-
msg.toLowerCase().includes('invalid'), `Expected symlink rejection, got: ${msg}`);
|
|
200
|
-
}
|
|
201
|
-
finally {
|
|
202
|
-
fs.rmSync(linkName, { force: true });
|
|
203
|
-
fs.rmSync(target, { force: true });
|
|
204
|
-
}
|
|
205
|
-
});
|
|
206
|
-
});
|