gitlab-mcp 1.1.0 → 1.2.1
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 +12 -1
- package/dist/config/dotenv.d.ts +2 -0
- package/dist/config/dotenv.js +40 -0
- package/dist/config/dotenv.js.map +1 -0
- package/dist/config/env.d.ts +55 -0
- package/dist/config/env.js +164 -0
- package/dist/config/env.js.map +1 -0
- package/dist/http-app.d.ts +45 -0
- package/dist/http-app.js +550 -0
- package/dist/http-app.js.map +1 -0
- package/dist/http.d.ts +2 -0
- package/dist/http.js +65 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +65 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/auth-context.d.ts +9 -0
- package/dist/lib/auth-context.js +9 -0
- package/dist/lib/auth-context.js.map +1 -0
- package/dist/lib/gitlab-client.d.ts +331 -0
- package/dist/lib/gitlab-client.js +1025 -0
- package/dist/lib/gitlab-client.js.map +1 -0
- package/dist/lib/logger.d.ts +2 -0
- package/dist/lib/logger.js +13 -0
- package/dist/lib/logger.js.map +1 -0
- package/dist/lib/network.d.ts +3 -0
- package/dist/lib/network.js +38 -0
- package/dist/lib/network.js.map +1 -0
- package/dist/lib/oauth.d.ts +29 -0
- package/dist/lib/oauth.js +220 -0
- package/dist/lib/oauth.js.map +1 -0
- package/dist/lib/output.d.ts +14 -0
- package/dist/lib/output.js +38 -0
- package/dist/lib/output.js.map +1 -0
- package/dist/lib/policy.d.ts +25 -0
- package/dist/lib/policy.js +48 -0
- package/dist/lib/policy.js.map +1 -0
- package/dist/lib/request-runtime.d.ts +26 -0
- package/dist/lib/request-runtime.js +323 -0
- package/dist/lib/request-runtime.js.map +1 -0
- package/dist/lib/sanitize.d.ts +1 -0
- package/dist/lib/sanitize.js +21 -0
- package/dist/lib/sanitize.js.map +1 -0
- package/dist/lib/session-capacity.d.ts +8 -0
- package/dist/lib/session-capacity.js +7 -0
- package/dist/lib/session-capacity.js.map +1 -0
- package/dist/server/build-server.d.ts +3 -0
- package/dist/server/build-server.js +13 -0
- package/dist/server/build-server.js.map +1 -0
- package/dist/tools/gitlab.d.ts +9 -0
- package/dist/tools/gitlab.js +2576 -0
- package/dist/tools/gitlab.js.map +1 -0
- package/dist/tools/health.d.ts +2 -0
- package/dist/tools/health.js +21 -0
- package/dist/tools/health.js.map +1 -0
- package/dist/tools/mr-code-context.d.ts +38 -0
- package/dist/tools/mr-code-context.js +330 -0
- package/dist/tools/mr-code-context.js.map +1 -0
- package/{src/types/context.ts → dist/types/context.d.ts} +5 -6
- package/dist/types/context.js +2 -0
- package/dist/types/context.js.map +1 -0
- package/docs/architecture.md +10 -10
- package/docs/configuration.md +12 -7
- package/docs/mcp-integration-testing-best-practices.md +981 -0
- package/package.json +13 -1
- package/.dockerignore +0 -7
- package/.editorconfig +0 -9
- package/.env.example +0 -75
- package/.github/workflows/nodejs.yml +0 -31
- package/.github/workflows/npm-publish.yml +0 -31
- package/.husky/pre-commit +0 -1
- package/.nvmrc +0 -1
- package/.prettierrc.json +0 -6
- package/Dockerfile +0 -20
- package/docker-compose.yml +0 -10
- package/eslint.config.js +0 -23
- package/scripts/get-oauth-token.example.sh +0 -15
- package/src/config/env.ts +0 -171
- package/src/http.ts +0 -620
- package/src/index.ts +0 -77
- package/src/lib/auth-context.ts +0 -19
- package/src/lib/gitlab-client.ts +0 -1810
- package/src/lib/logger.ts +0 -17
- package/src/lib/network.ts +0 -45
- package/src/lib/oauth.ts +0 -287
- package/src/lib/output.ts +0 -51
- package/src/lib/policy.ts +0 -78
- package/src/lib/request-runtime.ts +0 -376
- package/src/lib/sanitize.ts +0 -25
- package/src/lib/session-capacity.ts +0 -14
- package/src/server/build-server.ts +0 -17
- package/src/tools/gitlab.ts +0 -3135
- package/src/tools/health.ts +0 -27
- package/src/tools/mr-code-context.ts +0 -473
- package/tests/auth-context.test.ts +0 -102
- package/tests/gitlab-client.test.ts +0 -672
- package/tests/graphql-guard.test.ts +0 -121
- package/tests/integration/agent-loop.integration.test.ts +0 -558
- package/tests/integration/server.integration.test.ts +0 -543
- package/tests/mr-code-context.test.ts +0 -600
- package/tests/oauth.test.ts +0 -43
- package/tests/output.test.ts +0 -186
- package/tests/policy.test.ts +0 -324
- package/tests/request-runtime.test.ts +0 -252
- package/tests/sanitize.test.ts +0 -123
- package/tests/session-capacity.test.ts +0 -49
- package/tests/upload-reference.test.ts +0 -88
- package/tsconfig.build.json +0 -11
- package/tsconfig.json +0 -21
- package/vitest.config.ts +0 -12
|
@@ -1,543 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Integration tests for the MCP server using InMemoryTransport.
|
|
3
|
-
*
|
|
4
|
-
* These tests exercise the full server lifecycle: creating an MCP server,
|
|
5
|
-
* connecting a real MCP Client via InMemoryTransport, and verifying
|
|
6
|
-
* tools/list, tools/call, and protocol-level behavior.
|
|
7
|
-
*
|
|
8
|
-
* No external network calls are made; the GitLabClient is stubbed.
|
|
9
|
-
*/
|
|
10
|
-
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
|
11
|
-
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
12
|
-
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
13
|
-
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
14
|
-
|
|
15
|
-
import { createMcpServer } from "../../src/server/build-server.js";
|
|
16
|
-
import { OutputFormatter } from "../../src/lib/output.js";
|
|
17
|
-
import { ToolPolicyEngine } from "../../src/lib/policy.js";
|
|
18
|
-
import type { AppContext } from "../../src/types/context.js";
|
|
19
|
-
|
|
20
|
-
/* ------------------------------------------------------------------ */
|
|
21
|
-
/* Helpers */
|
|
22
|
-
/* ------------------------------------------------------------------ */
|
|
23
|
-
|
|
24
|
-
const defaultFeatures = {
|
|
25
|
-
wiki: true,
|
|
26
|
-
milestone: true,
|
|
27
|
-
pipeline: true,
|
|
28
|
-
release: true
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
/** Build a minimal AppContext with a stubbed GitLabClient. */
|
|
32
|
-
function buildContext(overrides?: {
|
|
33
|
-
readOnlyMode?: boolean;
|
|
34
|
-
allowedTools?: string[];
|
|
35
|
-
deniedToolsRegex?: RegExp;
|
|
36
|
-
enabledFeatures?: typeof defaultFeatures;
|
|
37
|
-
token?: string | null; // null = no token; undefined = use default
|
|
38
|
-
allowedProjectIds?: string[];
|
|
39
|
-
allowGraphqlWithProjectScope?: boolean;
|
|
40
|
-
}): AppContext {
|
|
41
|
-
const features = overrides?.enabledFeatures ?? defaultFeatures;
|
|
42
|
-
const readOnlyMode = overrides?.readOnlyMode ?? false;
|
|
43
|
-
const token = overrides?.token === null ? undefined : (overrides?.token ?? "test-token");
|
|
44
|
-
|
|
45
|
-
return {
|
|
46
|
-
env: {
|
|
47
|
-
NODE_ENV: "test",
|
|
48
|
-
LOG_LEVEL: "silent",
|
|
49
|
-
MCP_SERVER_NAME: "test-gitlab-mcp",
|
|
50
|
-
MCP_SERVER_VERSION: "0.0.1",
|
|
51
|
-
GITLAB_API_URL: "https://gitlab.example.com/api/v4",
|
|
52
|
-
GITLAB_API_URLS: ["https://gitlab.example.com/api/v4"],
|
|
53
|
-
GITLAB_PERSONAL_ACCESS_TOKEN: token,
|
|
54
|
-
GITLAB_USE_OAUTH: false,
|
|
55
|
-
GITLAB_OAUTH_AUTO_OPEN_BROWSER: false,
|
|
56
|
-
GITLAB_OAUTH_SCOPES: "api",
|
|
57
|
-
GITLAB_READ_ONLY_MODE: readOnlyMode,
|
|
58
|
-
GITLAB_ALLOWED_PROJECT_IDS: overrides?.allowedProjectIds ?? [],
|
|
59
|
-
GITLAB_ALLOWED_TOOLS: overrides?.allowedTools ?? [],
|
|
60
|
-
GITLAB_ALLOW_GRAPHQL_WITH_PROJECT_SCOPE: overrides?.allowGraphqlWithProjectScope ?? false,
|
|
61
|
-
GITLAB_RESPONSE_MODE: "json",
|
|
62
|
-
GITLAB_MAX_RESPONSE_BYTES: 200_000,
|
|
63
|
-
GITLAB_HTTP_TIMEOUT_MS: 20_000,
|
|
64
|
-
GITLAB_ERROR_DETAIL_MODE: "full",
|
|
65
|
-
GITLAB_CLOUDFLARE_BYPASS: false,
|
|
66
|
-
GITLAB_ALLOW_INSECURE_TOKEN_FILE: false,
|
|
67
|
-
GITLAB_ALLOW_INSECURE_TLS: false,
|
|
68
|
-
GITLAB_COOKIE_WARMUP_PATH: "/user",
|
|
69
|
-
USE_GITLAB_WIKI: features.wiki,
|
|
70
|
-
USE_MILESTONE: features.milestone,
|
|
71
|
-
USE_PIPELINE: features.pipeline,
|
|
72
|
-
USE_RELEASE: features.release,
|
|
73
|
-
REMOTE_AUTHORIZATION: false,
|
|
74
|
-
ENABLE_DYNAMIC_API_URL: false,
|
|
75
|
-
HTTP_JSON_ONLY: false,
|
|
76
|
-
SSE: false,
|
|
77
|
-
SESSION_TIMEOUT_SECONDS: 3600,
|
|
78
|
-
MAX_SESSIONS: 1000,
|
|
79
|
-
MAX_REQUESTS_PER_MINUTE: 300,
|
|
80
|
-
HTTP_HOST: "127.0.0.1",
|
|
81
|
-
HTTP_PORT: 3333,
|
|
82
|
-
GITLAB_TOKEN_CACHE_SECONDS: 300,
|
|
83
|
-
GITLAB_TOKEN_SCRIPT_TIMEOUT_MS: 10_000,
|
|
84
|
-
GITLAB_OAUTH_GITLAB_URL: undefined,
|
|
85
|
-
GITLAB_OAUTH_CLIENT_ID: undefined,
|
|
86
|
-
GITLAB_OAUTH_CLIENT_SECRET: undefined,
|
|
87
|
-
GITLAB_OAUTH_REDIRECT_URI: undefined,
|
|
88
|
-
GITLAB_OAUTH_TOKEN_PATH: undefined,
|
|
89
|
-
GITLAB_AUTH_COOKIE_PATH: undefined,
|
|
90
|
-
GITLAB_USER_AGENT: undefined,
|
|
91
|
-
GITLAB_ACCEPT_LANGUAGE: undefined,
|
|
92
|
-
GITLAB_TOKEN_SCRIPT: undefined,
|
|
93
|
-
GITLAB_TOKEN_FILE: undefined,
|
|
94
|
-
GITLAB_CA_CERT_PATH: undefined,
|
|
95
|
-
GITLAB_DENIED_TOOLS_REGEX: undefined,
|
|
96
|
-
NODE_TLS_REJECT_UNAUTHORIZED: undefined,
|
|
97
|
-
HTTP_PROXY: undefined,
|
|
98
|
-
HTTPS_PROXY: undefined
|
|
99
|
-
} as AppContext["env"],
|
|
100
|
-
logger: {
|
|
101
|
-
info: vi.fn(),
|
|
102
|
-
warn: vi.fn(),
|
|
103
|
-
error: vi.fn(),
|
|
104
|
-
debug: vi.fn(),
|
|
105
|
-
trace: vi.fn(),
|
|
106
|
-
fatal: vi.fn(),
|
|
107
|
-
child: () => ({}) as never
|
|
108
|
-
} as unknown as AppContext["logger"],
|
|
109
|
-
gitlab: {} as AppContext["gitlab"], // Stubbed — individual tools/call tests mock as needed
|
|
110
|
-
policy: new ToolPolicyEngine({
|
|
111
|
-
readOnlyMode,
|
|
112
|
-
allowedTools: overrides?.allowedTools ?? [],
|
|
113
|
-
deniedToolsRegex: overrides?.deniedToolsRegex,
|
|
114
|
-
enabledFeatures: features
|
|
115
|
-
}),
|
|
116
|
-
formatter: new OutputFormatter({
|
|
117
|
-
responseMode: "json",
|
|
118
|
-
maxBytes: 200_000
|
|
119
|
-
})
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/** Create linked Client + Server pair and connect them. */
|
|
124
|
-
async function createLinkedPair(context: AppContext): Promise<{
|
|
125
|
-
client: Client;
|
|
126
|
-
server: McpServer;
|
|
127
|
-
clientTransport: InMemoryTransport;
|
|
128
|
-
serverTransport: InMemoryTransport;
|
|
129
|
-
}> {
|
|
130
|
-
const server = createMcpServer(context);
|
|
131
|
-
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
132
|
-
|
|
133
|
-
await server.connect(serverTransport);
|
|
134
|
-
|
|
135
|
-
const client = new Client(
|
|
136
|
-
{ name: "integration-test-client", version: "0.0.1" },
|
|
137
|
-
{ capabilities: {} }
|
|
138
|
-
);
|
|
139
|
-
await client.connect(clientTransport);
|
|
140
|
-
|
|
141
|
-
return { client, server, clientTransport, serverTransport };
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/* ------------------------------------------------------------------ */
|
|
145
|
-
/* Tests: Server lifecycle & tools/list */
|
|
146
|
-
/* ------------------------------------------------------------------ */
|
|
147
|
-
|
|
148
|
-
describe("MCP Server Integration (InMemoryTransport)", () => {
|
|
149
|
-
let client: Client;
|
|
150
|
-
let clientTransport: InMemoryTransport;
|
|
151
|
-
let serverTransport: InMemoryTransport;
|
|
152
|
-
|
|
153
|
-
beforeAll(async () => {
|
|
154
|
-
const context = buildContext();
|
|
155
|
-
const pair = await createLinkedPair(context);
|
|
156
|
-
client = pair.client;
|
|
157
|
-
clientTransport = pair.clientTransport;
|
|
158
|
-
serverTransport = pair.serverTransport;
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
afterAll(async () => {
|
|
162
|
-
await clientTransport.close();
|
|
163
|
-
await serverTransport.close();
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
describe("protocol basics", () => {
|
|
167
|
-
it("completes initialization handshake", () => {
|
|
168
|
-
const serverVersion = client.getServerVersion();
|
|
169
|
-
expect(serverVersion).toBeDefined();
|
|
170
|
-
expect(serverVersion!.name).toBe("test-gitlab-mcp");
|
|
171
|
-
expect(serverVersion!.version).toBe("0.0.1");
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it("reports server capabilities including tools", () => {
|
|
175
|
-
const caps = client.getServerCapabilities();
|
|
176
|
-
expect(caps).toBeDefined();
|
|
177
|
-
expect(caps!.tools).toBeDefined();
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it("responds to ping", async () => {
|
|
181
|
-
const result = await client.ping();
|
|
182
|
-
expect(result).toBeDefined();
|
|
183
|
-
});
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
describe("tools/list", () => {
|
|
187
|
-
it("returns a non-empty list of tools", async () => {
|
|
188
|
-
const result = await client.listTools();
|
|
189
|
-
expect(result.tools.length).toBeGreaterThan(0);
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it("includes health_check tool", async () => {
|
|
193
|
-
const result = await client.listTools();
|
|
194
|
-
const names = result.tools.map((t) => t.name);
|
|
195
|
-
expect(names).toContain("health_check");
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it("includes core gitlab tools", async () => {
|
|
199
|
-
const result = await client.listTools();
|
|
200
|
-
const names = result.tools.map((t) => t.name);
|
|
201
|
-
|
|
202
|
-
// Spot-check a few essential tools
|
|
203
|
-
expect(names).toContain("gitlab_get_project");
|
|
204
|
-
expect(names).toContain("gitlab_list_projects");
|
|
205
|
-
expect(names).toContain("gitlab_get_file_contents");
|
|
206
|
-
expect(names).toContain("gitlab_create_merge_request");
|
|
207
|
-
expect(names).toContain("gitlab_list_issues");
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
it("every tool has a name and inputSchema", async () => {
|
|
211
|
-
const result = await client.listTools();
|
|
212
|
-
for (const tool of result.tools) {
|
|
213
|
-
expect(tool.name).toBeTruthy();
|
|
214
|
-
expect(tool.inputSchema).toBeDefined();
|
|
215
|
-
expect(tool.inputSchema.type).toBe("object");
|
|
216
|
-
}
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
it("every tool has a description", async () => {
|
|
220
|
-
const result = await client.listTools();
|
|
221
|
-
for (const tool of result.tools) {
|
|
222
|
-
expect(tool.description).toBeTruthy();
|
|
223
|
-
}
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
it("tool names follow naming convention", async () => {
|
|
227
|
-
const result = await client.listTools();
|
|
228
|
-
for (const tool of result.tools) {
|
|
229
|
-
// All tools should be either health_check or gitlab_*
|
|
230
|
-
expect(tool.name === "health_check" || tool.name.startsWith("gitlab_")).toBe(true);
|
|
231
|
-
}
|
|
232
|
-
});
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
describe("tools/call - health_check", () => {
|
|
236
|
-
it("returns ok status with timestamp", async () => {
|
|
237
|
-
const result = await client.callTool({ name: "health_check", arguments: {} });
|
|
238
|
-
expect(result.isError).toBeFalsy();
|
|
239
|
-
expect(result.content).toBeDefined();
|
|
240
|
-
expect(Array.isArray(result.content)).toBe(true);
|
|
241
|
-
|
|
242
|
-
const textContent = (result.content as Array<{ type: string; text: string }>).find(
|
|
243
|
-
(c) => c.type === "text"
|
|
244
|
-
);
|
|
245
|
-
expect(textContent).toBeDefined();
|
|
246
|
-
expect(textContent!.text).toContain("ok");
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
it("returns structuredContent with status and timestamp", async () => {
|
|
250
|
-
const result = await client.callTool({ name: "health_check", arguments: {} });
|
|
251
|
-
const structured = (result as { structuredContent?: Record<string, unknown> })
|
|
252
|
-
.structuredContent;
|
|
253
|
-
expect(structured).toBeDefined();
|
|
254
|
-
expect(structured!.status).toBe("ok");
|
|
255
|
-
expect(structured!.timestamp).toBeTruthy();
|
|
256
|
-
|
|
257
|
-
// Timestamp should be valid ISO 8601
|
|
258
|
-
const ts = structured!.timestamp as string;
|
|
259
|
-
expect(new Date(ts).toISOString()).toBe(ts);
|
|
260
|
-
});
|
|
261
|
-
});
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
/* ------------------------------------------------------------------ */
|
|
265
|
-
/* Tests: Policy enforcement through full MCP protocol */
|
|
266
|
-
/* ------------------------------------------------------------------ */
|
|
267
|
-
|
|
268
|
-
describe("MCP Server Integration - Read-only mode", () => {
|
|
269
|
-
let client: Client;
|
|
270
|
-
let clientTransport: InMemoryTransport;
|
|
271
|
-
let serverTransport: InMemoryTransport;
|
|
272
|
-
|
|
273
|
-
beforeAll(async () => {
|
|
274
|
-
const context = buildContext({ readOnlyMode: true });
|
|
275
|
-
const pair = await createLinkedPair(context);
|
|
276
|
-
client = pair.client;
|
|
277
|
-
clientTransport = pair.clientTransport;
|
|
278
|
-
serverTransport = pair.serverTransport;
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
afterAll(async () => {
|
|
282
|
-
await clientTransport.close();
|
|
283
|
-
await serverTransport.close();
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
it("excludes mutating tools from tools/list", async () => {
|
|
287
|
-
const result = await client.listTools();
|
|
288
|
-
const names = result.tools.map((t) => t.name);
|
|
289
|
-
|
|
290
|
-
// These are mutating tools that should be excluded in read-only mode
|
|
291
|
-
expect(names).not.toContain("gitlab_create_merge_request");
|
|
292
|
-
expect(names).not.toContain("gitlab_create_issue");
|
|
293
|
-
expect(names).not.toContain("gitlab_delete_issue");
|
|
294
|
-
expect(names).not.toContain("gitlab_create_or_update_file");
|
|
295
|
-
expect(names).not.toContain("gitlab_push_files");
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
it("still includes read-only tools", async () => {
|
|
299
|
-
const result = await client.listTools();
|
|
300
|
-
const names = result.tools.map((t) => t.name);
|
|
301
|
-
|
|
302
|
-
expect(names).toContain("health_check");
|
|
303
|
-
expect(names).toContain("gitlab_get_project");
|
|
304
|
-
expect(names).toContain("gitlab_list_projects");
|
|
305
|
-
expect(names).toContain("gitlab_get_file_contents");
|
|
306
|
-
});
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
describe("MCP Server Integration - Feature flag filtering", () => {
|
|
310
|
-
it("excludes wiki tools when wiki feature is disabled", async () => {
|
|
311
|
-
const context = buildContext({
|
|
312
|
-
enabledFeatures: { wiki: false, milestone: true, pipeline: true, release: true }
|
|
313
|
-
});
|
|
314
|
-
const { client, clientTransport, serverTransport } = await createLinkedPair(context);
|
|
315
|
-
|
|
316
|
-
try {
|
|
317
|
-
const result = await client.listTools();
|
|
318
|
-
const names = result.tools.map((t) => t.name);
|
|
319
|
-
|
|
320
|
-
expect(names).not.toContain("gitlab_list_wiki_pages");
|
|
321
|
-
expect(names).not.toContain("gitlab_get_wiki_page");
|
|
322
|
-
expect(names).not.toContain("gitlab_create_wiki_page");
|
|
323
|
-
// But other tools remain
|
|
324
|
-
expect(names).toContain("gitlab_get_project");
|
|
325
|
-
} finally {
|
|
326
|
-
await clientTransport.close();
|
|
327
|
-
await serverTransport.close();
|
|
328
|
-
}
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
it("excludes pipeline tools when pipeline feature is disabled", async () => {
|
|
332
|
-
const context = buildContext({
|
|
333
|
-
enabledFeatures: { wiki: true, milestone: true, pipeline: false, release: true }
|
|
334
|
-
});
|
|
335
|
-
const { client, clientTransport, serverTransport } = await createLinkedPair(context);
|
|
336
|
-
|
|
337
|
-
try {
|
|
338
|
-
const result = await client.listTools();
|
|
339
|
-
const names = result.tools.map((t) => t.name);
|
|
340
|
-
|
|
341
|
-
expect(names).not.toContain("gitlab_list_pipelines");
|
|
342
|
-
expect(names).not.toContain("gitlab_get_pipeline");
|
|
343
|
-
} finally {
|
|
344
|
-
await clientTransport.close();
|
|
345
|
-
await serverTransport.close();
|
|
346
|
-
}
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
it("excludes release tools when release feature is disabled", async () => {
|
|
350
|
-
const context = buildContext({
|
|
351
|
-
enabledFeatures: { wiki: true, milestone: true, pipeline: true, release: false }
|
|
352
|
-
});
|
|
353
|
-
const { client, clientTransport, serverTransport } = await createLinkedPair(context);
|
|
354
|
-
|
|
355
|
-
try {
|
|
356
|
-
const result = await client.listTools();
|
|
357
|
-
const names = result.tools.map((t) => t.name);
|
|
358
|
-
|
|
359
|
-
expect(names).not.toContain("gitlab_list_releases");
|
|
360
|
-
expect(names).not.toContain("gitlab_get_release");
|
|
361
|
-
expect(names).not.toContain("gitlab_create_release");
|
|
362
|
-
} finally {
|
|
363
|
-
await clientTransport.close();
|
|
364
|
-
await serverTransport.close();
|
|
365
|
-
}
|
|
366
|
-
});
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
describe("MCP Server Integration - Allowlist filtering", () => {
|
|
370
|
-
it("only exposes tools in the allowlist (plus health_check)", async () => {
|
|
371
|
-
const context = buildContext({
|
|
372
|
-
allowedTools: ["get_project", "list_projects"]
|
|
373
|
-
});
|
|
374
|
-
const { client, clientTransport, serverTransport } = await createLinkedPair(context);
|
|
375
|
-
|
|
376
|
-
try {
|
|
377
|
-
const result = await client.listTools();
|
|
378
|
-
const names = result.tools.map((t) => t.name);
|
|
379
|
-
|
|
380
|
-
expect(names).toContain("health_check"); // always present
|
|
381
|
-
expect(names).toContain("gitlab_get_project");
|
|
382
|
-
expect(names).toContain("gitlab_list_projects");
|
|
383
|
-
|
|
384
|
-
// Other tools should be excluded
|
|
385
|
-
expect(names).not.toContain("gitlab_create_issue");
|
|
386
|
-
expect(names).not.toContain("gitlab_get_file_contents");
|
|
387
|
-
} finally {
|
|
388
|
-
await clientTransport.close();
|
|
389
|
-
await serverTransport.close();
|
|
390
|
-
}
|
|
391
|
-
});
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
describe("MCP Server Integration - Denied tools regex", () => {
|
|
395
|
-
it("excludes tools matching denied regex pattern", async () => {
|
|
396
|
-
const context = buildContext({
|
|
397
|
-
deniedToolsRegex: /.*wiki.*/
|
|
398
|
-
});
|
|
399
|
-
const { client, clientTransport, serverTransport } = await createLinkedPair(context);
|
|
400
|
-
|
|
401
|
-
try {
|
|
402
|
-
const result = await client.listTools();
|
|
403
|
-
const names = result.tools.map((t) => t.name);
|
|
404
|
-
|
|
405
|
-
// Wiki tools should be excluded by regex
|
|
406
|
-
const wikiTools = names.filter((n) => n.includes("wiki"));
|
|
407
|
-
expect(wikiTools).toHaveLength(0);
|
|
408
|
-
|
|
409
|
-
// Other tools remain
|
|
410
|
-
expect(names).toContain("gitlab_get_project");
|
|
411
|
-
} finally {
|
|
412
|
-
await clientTransport.close();
|
|
413
|
-
await serverTransport.close();
|
|
414
|
-
}
|
|
415
|
-
});
|
|
416
|
-
});
|
|
417
|
-
|
|
418
|
-
/* ------------------------------------------------------------------ */
|
|
419
|
-
/* Tests: Error handling through full MCP protocol */
|
|
420
|
-
/* ------------------------------------------------------------------ */
|
|
421
|
-
|
|
422
|
-
describe("MCP Server Integration - Error handling", () => {
|
|
423
|
-
let client: Client;
|
|
424
|
-
let clientTransport: InMemoryTransport;
|
|
425
|
-
let serverTransport: InMemoryTransport;
|
|
426
|
-
|
|
427
|
-
beforeAll(async () => {
|
|
428
|
-
const context = buildContext();
|
|
429
|
-
const pair = await createLinkedPair(context);
|
|
430
|
-
client = pair.client;
|
|
431
|
-
clientTransport = pair.clientTransport;
|
|
432
|
-
serverTransport = pair.serverTransport;
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
afterAll(async () => {
|
|
436
|
-
await clientTransport.close();
|
|
437
|
-
await serverTransport.close();
|
|
438
|
-
});
|
|
439
|
-
|
|
440
|
-
it("returns error for unknown tool", async () => {
|
|
441
|
-
const result = await client.callTool({ name: "nonexistent_tool", arguments: {} });
|
|
442
|
-
expect(result.isError).toBe(true);
|
|
443
|
-
const textContent = (result.content as Array<{ type: string; text: string }>).find(
|
|
444
|
-
(c) => c.type === "text"
|
|
445
|
-
);
|
|
446
|
-
expect(textContent!.text).toContain("not found");
|
|
447
|
-
});
|
|
448
|
-
|
|
449
|
-
it("gitlab tools return error content when API call fails", async () => {
|
|
450
|
-
// gitlab_get_project will attempt to call context.gitlab.getProject
|
|
451
|
-
// which is a stub ({}) and will throw a TypeError.
|
|
452
|
-
// The tool handler catches errors and returns isError: true
|
|
453
|
-
const result = await client.callTool({
|
|
454
|
-
name: "gitlab_get_project",
|
|
455
|
-
arguments: { project_id: "test/project" }
|
|
456
|
-
});
|
|
457
|
-
|
|
458
|
-
expect(result.isError).toBe(true);
|
|
459
|
-
expect(result.content).toBeDefined();
|
|
460
|
-
});
|
|
461
|
-
});
|
|
462
|
-
|
|
463
|
-
/* ------------------------------------------------------------------ */
|
|
464
|
-
/* Tests: Multiple server configurations */
|
|
465
|
-
/* ------------------------------------------------------------------ */
|
|
466
|
-
|
|
467
|
-
describe("MCP Server Integration - No auth configured", () => {
|
|
468
|
-
it("returns error when calling tool that requires auth without token", async () => {
|
|
469
|
-
const context = buildContext({ token: null });
|
|
470
|
-
const { client, clientTransport, serverTransport } = await createLinkedPair(context);
|
|
471
|
-
|
|
472
|
-
try {
|
|
473
|
-
const result = await client.callTool({
|
|
474
|
-
name: "gitlab_get_project",
|
|
475
|
-
arguments: { project_id: "test/project" }
|
|
476
|
-
});
|
|
477
|
-
|
|
478
|
-
expect(result.isError).toBe(true);
|
|
479
|
-
const textContent = (result.content as Array<{ type: string; text: string }>).find(
|
|
480
|
-
(c) => c.type === "text"
|
|
481
|
-
);
|
|
482
|
-
expect(textContent!.text).toContain("Authentication required");
|
|
483
|
-
} finally {
|
|
484
|
-
await clientTransport.close();
|
|
485
|
-
await serverTransport.close();
|
|
486
|
-
}
|
|
487
|
-
});
|
|
488
|
-
|
|
489
|
-
it("health_check still works without auth", async () => {
|
|
490
|
-
const context = buildContext({ token: null });
|
|
491
|
-
const { client, clientTransport, serverTransport } = await createLinkedPair(context);
|
|
492
|
-
|
|
493
|
-
try {
|
|
494
|
-
const result = await client.callTool({ name: "health_check", arguments: {} });
|
|
495
|
-
expect(result.isError).toBeFalsy();
|
|
496
|
-
} finally {
|
|
497
|
-
await clientTransport.close();
|
|
498
|
-
await serverTransport.close();
|
|
499
|
-
}
|
|
500
|
-
});
|
|
501
|
-
});
|
|
502
|
-
|
|
503
|
-
/* ------------------------------------------------------------------ */
|
|
504
|
-
/* Tests: GraphQL tool filtering */
|
|
505
|
-
/* ------------------------------------------------------------------ */
|
|
506
|
-
|
|
507
|
-
describe("MCP Server Integration - GraphQL tool filtering", () => {
|
|
508
|
-
it("disables graphql tools when project scope is set without override", async () => {
|
|
509
|
-
const context = buildContext({
|
|
510
|
-
allowedProjectIds: ["group/project"],
|
|
511
|
-
allowGraphqlWithProjectScope: false
|
|
512
|
-
});
|
|
513
|
-
const { client, clientTransport, serverTransport } = await createLinkedPair(context);
|
|
514
|
-
|
|
515
|
-
try {
|
|
516
|
-
const result = await client.listTools();
|
|
517
|
-
const names = result.tools.map((t) => t.name);
|
|
518
|
-
|
|
519
|
-
expect(names).not.toContain("gitlab_execute_graphql");
|
|
520
|
-
} finally {
|
|
521
|
-
await clientTransport.close();
|
|
522
|
-
await serverTransport.close();
|
|
523
|
-
}
|
|
524
|
-
});
|
|
525
|
-
|
|
526
|
-
it("keeps graphql tools when project scope is set with override", async () => {
|
|
527
|
-
const context = buildContext({
|
|
528
|
-
allowedProjectIds: ["group/project"],
|
|
529
|
-
allowGraphqlWithProjectScope: true
|
|
530
|
-
});
|
|
531
|
-
const { client, clientTransport, serverTransport } = await createLinkedPair(context);
|
|
532
|
-
|
|
533
|
-
try {
|
|
534
|
-
const result = await client.listTools();
|
|
535
|
-
const names = result.tools.map((t) => t.name);
|
|
536
|
-
|
|
537
|
-
expect(names).toContain("gitlab_execute_graphql");
|
|
538
|
-
} finally {
|
|
539
|
-
await clientTransport.close();
|
|
540
|
-
await serverTransport.close();
|
|
541
|
-
}
|
|
542
|
-
});
|
|
543
|
-
});
|