From fabf3962abf2470e587ecd44de3dd9b9770ac692 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Fri, 30 Oct 2026 00:34:48 +0200 Subject: [PATCH 1/3] test(mcp): qualify Antigravity list results Exercise the installed core package through the actual Antigravity CLI with isolated synthetic GitLab and Gemini endpoints. Compare list, object, native and error results with the integrity-verified historical Gemini transport. Document client setup, migration and repeatable qualification. Closes #716 --- .../gitlab-mcp/docs/.vitepress/config.mts | 1 + .../gitlab-mcp/docs/clients/antigravity.md | 96 +++++ packages/gitlab-mcp/docs/clients/index.md | 1 + packages/gitlab-mcp/package.json | 1 + .../scripts/antigravity-contract.mjs | 97 +++++ .../gitlab-mcp/scripts/evaluation-process.mjs | 3 +- .../scripts/qualify-antigravity.mjs | 341 ++++++++++++++++++ .../manual/fixtures/mcp-envelope-recorder.mjs | 27 ++ .../tests/manual/fixtures/scripted-gemini.mjs | 68 ++++ .../tests/manual/fixtures/workflow-gitlab.mjs | 4 +- .../scripts/antigravity-results.test.mjs | 178 +++++++++ .../gitlab-mcp/tests/unit/evaluation.test.ts | 14 +- 12 files changed, 823 insertions(+), 8 deletions(-) create mode 100644 packages/gitlab-mcp/docs/clients/antigravity.md create mode 100644 packages/gitlab-mcp/scripts/antigravity-contract.mjs create mode 100644 packages/gitlab-mcp/scripts/qualify-antigravity.mjs create mode 100644 packages/gitlab-mcp/tests/manual/fixtures/mcp-envelope-recorder.mjs create mode 100644 packages/gitlab-mcp/tests/manual/fixtures/scripted-gemini.mjs create mode 100644 packages/gitlab-mcp/tests/scripts/antigravity-results.test.mjs diff --git a/packages/gitlab-mcp/docs/.vitepress/config.mts b/packages/gitlab-mcp/docs/.vitepress/config.mts index b0b91313..446a1154 100644 --- a/packages/gitlab-mcp/docs/.vitepress/config.mts +++ b/packages/gitlab-mcp/docs/.vitepress/config.mts @@ -306,6 +306,7 @@ export default defineConfig({ { text: "Overview", link: "/clients/" }, { text: "Claude Desktop", link: "/clients/claude-desktop" }, { text: "Claude Code", link: "/clients/claude-code" }, + { text: "Antigravity CLI", link: "/clients/antigravity" }, { text: "Cursor", link: "/clients/cursor" }, { text: "VS Code", link: "/clients/vscode" }, { text: "Windsurf", link: "/clients/windsurf" }, diff --git a/packages/gitlab-mcp/docs/clients/antigravity.md b/packages/gitlab-mcp/docs/clients/antigravity.md new file mode 100644 index 00000000..fd4992ae --- /dev/null +++ b/packages/gitlab-mcp/docs/clients/antigravity.md @@ -0,0 +1,96 @@ +--- +title: Antigravity CLI +description: Connect Antigravity CLI to GitLab MCP and reproduce the installed-client result qualification. +--- + +# Antigravity CLI + +Antigravity CLI (`agy`) is Google's successor to Gemini CLI. Configure GitLab MCP +in your workspace's `.agents/mcp_config.json`: + +```json +{ + "mcpServers": { + "gitlab": { + "command": "npx", + "args": ["-y", "@structured-world/gitlab-mcp@latest", "stdio"], + "env": { + "GITLAB_API_URL": "https://gitlab.com", + "GITLAB_TOKEN": "" + } + } + } +} +``` + +Use Node.js 24 or later. Set your GitLab instance URL and token as described in +[authentication](/guide/authentication). Keep credentials out of version control. +The setup wizard does not currently detect or configure AGY automatically. + +For an existing HTTP deployment, use `serverUrl` instead of `command`, `args` and +`env`. See [remote deployment](/deployment/) for the server configuration. + +AGY's global MCP configuration is `~/.gemini/config/mcp_config.json`. Its CLI +preferences are a separate file, `~/.gemini/antigravity-cli/settings.json`. +For migration from Gemini CLI, follow Google's +[migration guide](https://antigravity.google/docs/cli/gcli-migration/). +Copy only the servers you intend to use and avoid configuring the same server +in both workspace and global configuration. + +## JSON lists and structured results + +GitLab MCP's entity tools return JSON in a text content block. A list can be `[]` +or an array of entities. The optional MCP `structuredContent` field has a different +contract: it must be an object. Tools that explicitly declare structured output, +such as `get_profile`, retain that object and their output schema. See the +[MCP tool result specification](https://modelcontextprotocol.io/specification/2025-11-25/server/tools#structured-content). + +Installed-client qualification on October 30, 2026 used AGY **1.4.10**, macOS arm64 +and Node.js **26.8.1**, with a locally built core package and MCP SDK **1.32.2**. +The server connected to a synthetic GitLab HTTP fixture. A scripted loopback Gemini +endpoint supplied tool calls, so the run used no real credentials or model inference. + +| Scenario | Server MCP validation | AGY result | Gemini CLI Core 0.66.0 transport | +| ----------------------- | ------------------------------ | ------------------------------- | -------------------------------------- | +| Empty MR list | Valid text `[]` | Preserved | Adds invalid array `structuredContent` | +| One MR | Valid text array | Preserved, all fields unchanged | Adds invalid array `structuredContent` | +| Project object | Valid text object | Preserved | Valid object `structuredContent` | +| Account profile | Valid native structured object | Text preserved | Native envelope preserved | +| GitLab permission error | Valid `isError` result | Preserved as a tool error | Valid error envelope | + +The historical behavior was reproduced using the actual integrity-checked published +`@google/gemini-cli-core@0.66.0` transport. The current AGY path passed both list +cases. Existing Codex and Claude Code text results remain unchanged. + +If an old Gemini CLI reports that array `structuredContent` is invalid, migrate to +the current AGY client. AGY's `stream-json` tool output is a client display result, +not a second MCP envelope. The qualification validates the original MCP envelope +and the historical transport's modified envelope with the SDK, then independently +checks AGY's decoded output and the subsequent provider tool response. + +## Repeat the qualification + +From a repository checkout with dependencies installed: + +```bash +yarn workspace @structured-world/gitlab-mcp qualify:antigravity --report /tmp/antigravity-results.json +``` + +The command builds and packs the core package, installs the archive in a temporary +workspace, downloads the current official native AGY release and verifies its +SHA-512 checksum. It also verifies the pinned historical Gemini archive's npm +integrity. Each scenario gets its own client home, workspace MCP configuration +and allow rules for the selected read-only tool. Your existing client settings +and authentication are not imported. + +The JSON report records versions, archive checksums, selected tools and arguments, +server wire messages, SDK validation failures from the historical client, +AGY transcripts and provider responses. Temporary binaries, dependencies and client +profiles are removed after the report is written. Build output follows the +repository's normal build lifecycle. + +The native qualification command supports macOS and Linux. This result qualifies +stdio and the synthetic scenarios above; it does not qualify every transport or +replace testing against your GitLab instance. Downloads require network access; +all GitLab and provider traffic in the scenarios stays on loopback. No external +model credentials are needed. diff --git a/packages/gitlab-mcp/docs/clients/index.md b/packages/gitlab-mcp/docs/clients/index.md index ce723b0b..4099ef40 100644 --- a/packages/gitlab-mcp/docs/clients/index.md +++ b/packages/gitlab-mcp/docs/clients/index.md @@ -23,6 +23,7 @@ GitLab MCP works with all major AI coding assistants that support the Model Cont | [Cline](/clients/cline) | stdio | Extension dir | JSON config | | [Roo Code](/clients/roo-code) | stdio | Config dir | JSON config | | [Codex](/guide/installation/codex) | stdio or HTTP | CLI command | Plugin or `codex mcp` | +| [Antigravity CLI](/clients/antigravity) | stdio or HTTP | No | Workspace JSON | All clients can also connect via HTTP (StreamableHTTP or SSE) to a [remote server deployment](/deployment/). diff --git a/packages/gitlab-mcp/package.json b/packages/gitlab-mcp/package.json index 77b81d63..407a6591 100644 --- a/packages/gitlab-mcp/package.json +++ b/packages/gitlab-mcp/package.json @@ -592,6 +592,7 @@ "test:all": "yarn test:jest --maxWorkers=1 --workerIdleMemoryLimit=1GiB", "test:env-gating": "node tests/scripts/test-env-gating.js", "evaluate:skills": "node scripts/evaluate-skills.mjs", + "qualify:antigravity": "yarn build && node scripts/qualify-antigravity.mjs", "test:help": "printf '# Test Scripts\\n yarn test - Unit tests only (fast, safe default)\\n yarn test - Unit tests matching pattern\\n yarn test:cov - Unit test coverage\\n yarn test:integration - Integration tests (needs .env.test)\\n yarn test:all - Full suite (unit + integration)\\n yarn test:env-gating - Test environment gating\\n'", "cleanup:test-groups": "node scripts/cleanup-test-groups.js", "cleanup:test-groups:dry-run": "node scripts/cleanup-test-groups.js --dry-run", diff --git a/packages/gitlab-mcp/scripts/antigravity-contract.mjs b/packages/gitlab-mcp/scripts/antigravity-contract.mjs new file mode 100644 index 00000000..c65d80f0 --- /dev/null +++ b/packages/gitlab-mcp/scripts/antigravity-contract.mjs @@ -0,0 +1,97 @@ +import assert from 'node:assert/strict'; +import { CallToolRequestSchema, CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js'; +import { AjvJsonSchemaValidator } from '@modelcontextprotocol/sdk/validation/ajv-provider.js'; + +/** Validate the envelope independently of the client's display representation. */ +export function validateEnvelope(result, tool) { + CallToolResultSchema.parse(result); + if (tool.outputSchema && !result.isError) { + assert.notEqual(result.structuredContent, undefined, 'Missing required structured result'); + const validation = new AjvJsonSchemaValidator().getValidator(tool.outputSchema)( + result.structuredContent, + ); + assert.equal(validation.valid, true, validation.errorMessage); + } +} + +/** Correlate actual request IDs, including the tool definition received by the client. */ +export function recordedCall(records, expected) { + const requests = records.filter( + ({ direction, message }) => direction === 'request' && message.method === 'tools/call', + ); + assert.equal(requests.length, 1, 'Expected exactly one MCP tool invocation'); + const request = requests[0].message; + CallToolRequestSchema.parse(request); + // Client-owned request metadata stays in the capture, outside the tool's input contract. + assert.deepEqual({ name: request.params.name, arguments: request.params.arguments }, expected); + const response = records.find( + ({ direction, message }) => direction === 'response' && message.id === request.id, + ); + assert.ok(response?.message.result, 'Missing MCP tool result'); + const definitions = records.filter( + ({ direction, message }) => direction === 'response' && Array.isArray(message.result?.tools), + ); + const tool = definitions + .flatMap(({ message }) => message.result.tools) + .find(({ name }) => name === expected.name); + assert.ok(tool, 'Invoked tool was not advertised'); + validateEnvelope(response.message.result, tool); + return { request, result: response.message.result, tool }; +} + +/** Exercise the actual published transport, never a copied implementation of its fix. */ +export function historicalResult(McpComplianceTransport, original) { + const underlying = { start: async () => {}, close: async () => {}, send: async () => {} }; + const transport = new McpComplianceTransport(underlying); + let received; + transport.onmessage = (message) => { + received = message; + }; + underlying.onmessage({ jsonrpc: '2.0', id: 1, result: globalThis.structuredClone(original) }); + assert.ok(received, 'Historical transport did not deliver its response'); + const validation = CallToolResultSchema.safeParse(received.result); + return { + result: received.result, + valid: validation.success, + issues: validation.success ? [] : validation.error.issues, + }; +} + +/** Check the CLI's actual decoded result and the provider's subsequent tool response. */ +export function clientResult(events, requests, expected, text, isError) { + const tools = events.filter( + ({ step_update: step }) => step?.step_type === 'tool' && ['DONE', 'ERROR'].includes(step.state), + ); + assert.equal(tools.length, 1, 'Expected one completed AGY tool'); + const completed = tools[0].step_update; + assert.equal(completed.state, isError ? 'ERROR' : 'DONE'); + assert.equal(completed.tool_name, 'call_mcp_tool'); + assert.deepEqual(completed.tool_info.parameters, { + ServerName: 'gitlab', + ToolName: expected.name, + Arguments: expected.arguments, + }); + assert.equal(completed.tool_info.output, text, 'Client changed the result text'); + if (isError) assert.deepEqual(completed.tool_info.error, { type: 'TOOL_ERROR', message: text }); + else assert.equal(Object.hasOwn(completed.tool_info, 'error'), false); + const replies = requests + .flatMap(({ body }) => body.contents.flatMap(({ parts }) => parts)) + .filter(({ functionResponse }) => functionResponse?.name === 'call_mcp_tool'); + assert.equal(replies.length, 1, 'Expected exactly one provider tool response'); + const response = replies[0].functionResponse.response; + const key = isError ? 'error' : 'output'; + assert.deepEqual(Object.keys(response), [key]); + const lines = response[key].split('\n'); + for (const [index, label] of ['Created At: ', 'Completed At: '].entries()) { + assert.ok(lines[index].startsWith(label)); + assert.equal(Number.isNaN(Date.parse(lines[index].slice(label.length))), false); + } + assert.equal( + lines.slice(2).join('\n'), + isError ? `Encountered error in tool execution: ${text}` : text, + ); + const final = events.filter(({ event }) => event === 'result'); + assert.equal(final.length, 1); + assert.equal(final[0].result.status, 'SUCCESS'); + return { decoded: completed.tool_info, providerResponse: response }; +} diff --git a/packages/gitlab-mcp/scripts/evaluation-process.mjs b/packages/gitlab-mcp/scripts/evaluation-process.mjs index f36fc93b..62477ae9 100644 --- a/packages/gitlab-mcp/scripts/evaluation-process.mjs +++ b/packages/gitlab-mcp/scripts/evaluation-process.mjs @@ -33,11 +33,12 @@ export function launchSync(command, args, options) { return result.stdout; } -export function runEvaluation(command, args, { cwd, timeoutMs = 180000 }) { +export function runEvaluation(command, args, { cwd, env, timeoutMs = 180000 }) { const taskkill = process.platform === 'win32' ? resolveExecutable('taskkill') : undefined; return new Promise((resolveRun, reject) => { const child = launch(command, args, { cwd, + env, detached: process.platform !== 'win32', stdio: ['ignore', 'pipe', 'pipe'], }); diff --git a/packages/gitlab-mcp/scripts/qualify-antigravity.mjs b/packages/gitlab-mcp/scripts/qualify-antigravity.mjs new file mode 100644 index 00000000..b5aa1812 --- /dev/null +++ b/packages/gitlab-mcp/scripts/qualify-antigravity.mjs @@ -0,0 +1,341 @@ +import assert from 'node:assert/strict'; +import { createHash } from 'node:crypto'; +import { openSync, closeSync } from 'node:fs'; +import { mkdir, mkdtemp, readFile, writeFile, rm, realpath } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { parse as parseYaml } from 'yaml'; +import { launchSync, resolveExecutable, runEvaluation } from './evaluation-process.mjs'; +import { recordedCall, historicalResult, clientResult } from './antigravity-contract.mjs'; +import { startWorkflowGitLab } from '../tests/manual/fixtures/workflow-gitlab.mjs'; +import { startScriptedGemini } from '../tests/manual/fixtures/scripted-gemini.mjs'; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const gemini = { + version: '0.66.0', + url: 'https://registry.npmjs.org/@google/gemini-cli-core/-/gemini-cli-core-0.66.0.tgz', + integrity: + 'sha512-fhrC9UfDY91VK+bCz7fZQE0NBas2TgwYeF099cG2TVYeoxwL2g4FaHC6pPHYfk14i4Sd1xAeboLEtOAPelrcLA==', + entry: 'package/dist/src/tools/mcp-compliance-transport.js', +}; + +async function download(url) { + const response = await fetch(url, { signal: globalThis.AbortSignal.timeout(120000) }); + assert.equal(response.ok, true, `Download failed: HTTP ${response.status}`); + const chunks = []; + let length = 0; + for await (const chunk of response.body) { + length += chunk.length; + assert.ok(length <= 128 * 1024 * 1024, 'Download exceeds fixture limit'); + chunks.push(chunk); + } + return Buffer.concat(chunks); +} + +function digest(bytes, encoding = 'hex') { + return createHash('sha512').update(bytes).digest(encoding); +} + +async function prepare(runtime, archive) { + assert.ok( + ['darwin', 'linux'].includes(process.platform), + 'Qualification requires macOS or Linux', + ); + const architecture = { arm64: 'arm64', x64: 'amd64' }[process.arch]; + assert.ok(architecture, 'Unsupported client architecture'); + const manifestUrl = `https://antigravity-cli-auto-updater-974169037036.us-central1.run.app/manifests/${process.platform}_${architecture}.json`; + const manifest = JSON.parse(await download(manifestUrl)); + const bytes = archive ? await readFile(archive) : await download(manifest.url); + assert.equal(digest(bytes), manifest.sha512, 'AGY archive checksum mismatch'); + const archivePath = join(runtime, 'agy.tgz'); + await writeFile(archivePath, bytes); + const tar = resolveExecutable('tar'); + const command = join(runtime, 'antigravity'); + const executable = openSync(command, 'wx', 0o755); + try { + launchSync(tar, ['-xOf', archivePath, 'antigravity'], { + stdio: ['ignore', executable, 'pipe'], + timeout: 30000, + }); + } finally { + closeSync(executable); + } + const baseline = await download(gemini.url); + assert.equal(`sha512-${digest(baseline, 'base64')}`, gemini.integrity); + const baselinePath = join(runtime, 'gemini.tgz'); + await writeFile(baselinePath, baseline); + const source = launchSync(tar, ['-xOf', baselinePath, gemini.entry], { timeout: 30000 }); + const { McpComplianceTransport } = await import( + `data:text/javascript;base64,${source.toString('base64')}` + ); + const monorepo = resolve(root, '../..'); + const yarnConfiguration = parseYaml(await readFile(join(monorepo, '.yarnrc.yml'), 'utf8')); + assert.equal(typeof yarnConfiguration.yarnPath, 'string'); + const yarn = resolve(monorepo, yarnConfiguration.yarnPath); + const artifact = join(runtime, 'package.tgz'); + launchSync(process.execPath, [yarn, 'pack', '--out', artifact], { + cwd: root, + stdio: 'pipe', + timeout: 60000, + }); + await writeFile( + join(runtime, 'package.json'), + JSON.stringify({ + name: 'mcp-client-qualification', + private: true, + dependencies: { '@structured-world/gitlab-mcp': 'file:./package.tgz' }, + }), + ); + await writeFile( + join(runtime, '.yarnrc.yml'), + 'nodeLinker: node-modules\nenableGlobalCache: false\n', + ); + const installation = await runEvaluation( + process.execPath, + [yarn, 'install', '--no-immutable', '--mode=skip-build'], + { + cwd: runtime, + timeoutMs: 120000, + }, + ); + assert.equal(installation.code, 0, installation.stderr + installation.stdout); + const installed = join(runtime, 'node_modules/@structured-world/gitlab-mcp'); + const installedPackage = JSON.parse(await readFile(join(installed, 'package.json'), 'utf8')); + const installedSdk = JSON.parse( + await readFile(join(runtime, 'node_modules/@modelcontextprotocol/sdk/package.json'), 'utf8'), + ); + return { + command, + manifest, + manifestUrl, + McpComplianceTransport, + entry: join(installed, 'dist/src/main.js'), + artifact: { + version: installedPackage.version, + sha512: digest(await readFile(artifact)), + sdkVersion: installedSdk.version, + yarnVersion: launchSync(process.execPath, [yarn, '--version'], { + cwd: runtime, + encoding: 'utf8', + }).trim(), + }, + baseline: { ...gemini, moduleSha512: digest(source) }, + }; +} + +function isolatedEnvironment(profile) { + // HOME is the child client's actual isolated home, never a change to the caller's environment. + const environment = { + HOME: profile, + XDG_CONFIG_HOME: join(profile, 'config'), + XDG_DATA_HOME: join(profile, 'data'), + GEMINI_API_KEY: 'fixture-only', + }; + for (const name of ['PATH', 'TMPDIR', 'TMP', 'TEMP']) + if (process.env[name] !== undefined) environment[name] = process.env[name]; + return environment; +} + +async function scenario(runtime, prepared, spec) { + const workspace = join(runtime, spec.name); + const profile = join(workspace, 'home'); + await mkdir(join(workspace, '.agents'), { recursive: true }); + await mkdir(profile); + const environment = isolatedEnvironment(profile); + const version = await runEvaluation(prepared.command, ['--version'], { + cwd: workspace, + env: environment, + timeoutMs: 30000, + }); + assert.equal(version.code, 0, version.stderr); + assert.equal(version.stdout.trim(), prepared.manifest.version); + const fixture = await startWorkflowGitLab({ emptyMergeRequests: spec.empty === true }); + const provider = await startScriptedGemini(spec.call); + const recording = join(workspace, 'mcp.jsonl'); + let result; + let records; + try { + await writeFile( + join(workspace, '.agents/mcp_config.json'), + JSON.stringify({ + mcpServers: { + gitlab: { + command: process.execPath, + args: [ + join(root, 'tests/manual/fixtures/mcp-envelope-recorder.mjs'), + prepared.entry, + recording, + ], + env: { + GITLAB_API_URL: fixture.url, + GITLAB_TOKEN: 'fixture-only', + OAUTH_ENABLED: 'false', + LOG_LEVEL: 'error', + GITLAB_READ_ONLY_MODE: 'true', + }, + enabledTools: [spec.call.name], + }, + }, + }), + ); + const settings = { + modelProvider: 'gemini', + trustedWorkspaces: [workspace], + permissions: { allow: [`mcp(gitlab/${spec.call.name})`] }, + customModelsConfig: { + customModels: { + fixture: { + modelName: 'gemini-api://fixture/models/qualification', + toolFormatterType: 'TOOL_FORMATTER_TYPE_NONE', + }, + }, + }, + }; + result = await runEvaluation( + prepared.command, + [ + '--settings', + JSON.stringify(settings), + '--model', + 'fixture', + '--print', + 'Run the synthetic MCP qualification.', + '--output-format', + 'stream-json', + '--print-timeout', + '60s', + ], + { + cwd: workspace, + env: { ...environment, GOOGLE_GEMINI_BASE_URL: provider.url }, + timeoutMs: 75000, + }, + ); + assert.equal(result.code, 0, result.stderr); + provider.verify(); + records = (await readFile(recording, 'utf8')).trim().split('\n').map(JSON.parse); + const call = recordedCall(records, spec.call); + const events = result.stdout.trim().split('\n').map(JSON.parse); + const text = call.result.content + .map(({ type, text }) => { + assert.equal(type, 'text'); + return text; + }) + .join('\n'); + const client = clientResult(events, provider.requests, spec.call, text, spec.error === true); + assert.equal(call.result.isError === true, spec.error === true); + const data = JSON.parse(text); + if (spec.empty !== undefined) { + assert.deepEqual(data, spec.empty ? [] : [fixture.mr]); + assert.equal(Object.hasOwn(call.result, 'structuredContent'), false); + assert.equal(Object.hasOwn(call.tool, 'outputSchema'), false); + } + if (spec.name === 'object') assert.deepEqual(data, fixture.project); + if (spec.name === 'native') assert.deepEqual(call.result.structuredContent, data); + const historical = historicalResult(prepared.McpComplianceTransport, call.result); + assert.equal(historical.valid, spec.empty === undefined); + if (spec.name === 'native') assert.deepEqual(historical.result, call.result); + return { + name: spec.name, + status: 'verified', + ...call, + clientResult: client, + historical, + providerRequests: provider.requests, + wire: records, + transcript: events, + gitlabRequests: fixture.requests, + }; + } catch (error) { + throw Object.assign(error, { + evidence: { + name: spec.name, + result, + wire: records, + gitlabRequests: fixture.requests, + providerRequests: provider.requests, + }, + }); + } finally { + await provider.close(); + await fixture.close(); + } +} + +async function main() { + const args = process.argv.slice(2); + let archive; + let reportPath; + while (args.length) { + const flag = args.shift(); + assert.ok(args.length, `Missing value for ${flag}`); + if (flag === '--agy-archive') archive = resolve(args.shift()); + else if (flag === '--report') reportPath = resolve(args.shift()); + else throw new Error(`Unknown argument: ${flag}`); + } + const directory = await realpath(await mkdtemp(join(tmpdir(), 'gitlab-antigravity-'))); + const runtime = join(directory, 'runtime'); + await mkdir(runtime); + if (!reportPath) reportPath = join(directory, 'report.json'); + const report = { + status: 'failed', + nodeVersion: process.version, + platform: process.platform, + sourceCommit: launchSync(resolveExecutable('git'), ['rev-parse', 'HEAD'], { + cwd: root, + encoding: 'utf8', + }).trim(), + sourceDirty: + launchSync(resolveExecutable('git'), ['status', '--porcelain'], { + cwd: root, + encoding: 'utf8', + }).length > 0, + scenarios: [], + }; + try { + const prepared = await prepare(runtime, archive); + report.client = { + version: prepared.manifest.version, + manifestUrl: prepared.manifestUrl, + ...prepared.manifest, + }; + report.artifact = prepared.artifact; + report.historical = prepared.baseline; + const list = { + name: 'browse_merge_requests', + arguments: { action: 'list', project_id: 'test/backend' }, + }; + for (const spec of [ + { name: 'empty', empty: true, call: list }, + { name: 'single', empty: false, call: list }, + { + name: 'object', + call: { name: 'browse_projects', arguments: { action: 'get', project_id: 'test/backend' } }, + }, + { name: 'native', call: { name: 'get_profile', arguments: {} } }, + { + name: 'error', + error: true, + call: { name: 'browse_projects', arguments: { action: 'get', project_id: 'test/denied' } }, + }, + ]) { + report.scenarios.push(await scenario(runtime, prepared, spec)); + console.log(`${spec.name}: verified`); + } + report.status = 'verified'; + } catch (error) { + report.error = { message: error.message, evidence: error.evidence }; + process.exitCode = 1; + } finally { + try { + await writeFile(reportPath, JSON.stringify(report, null, 2) + '\n'); + console.log(`${report.status}: ${reportPath}`); + } finally { + await rm(runtime, { recursive: true, force: true }); + if (dirname(reportPath) !== directory) await rm(directory, { recursive: true }); + } + } +} + +if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) await main(); diff --git a/packages/gitlab-mcp/tests/manual/fixtures/mcp-envelope-recorder.mjs b/packages/gitlab-mcp/tests/manual/fixtures/mcp-envelope-recorder.mjs new file mode 100644 index 00000000..70491536 --- /dev/null +++ b/packages/gitlab-mcp/tests/manual/fixtures/mcp-envelope-recorder.mjs @@ -0,0 +1,27 @@ +import { spawn } from 'node:child_process'; +import { appendFileSync } from 'node:fs'; +import { createInterface } from 'node:readline'; + +// Observe the installed server's wire messages without changing their bytes. +const [entry, recording] = process.argv.slice(2); +if (!entry || !recording) throw new Error('Expected installed entry and recording path'); +const child = spawn(process.execPath, [entry, 'stdio'], { stdio: ['pipe', 'pipe', 'inherit'] }); +process.stdin.pipe(child.stdin); +child.stdout.pipe(process.stdout); +for (const [direction, stream] of [ + ['request', process.stdin], + ['response', child.stdout], +]) { + createInterface({ input: stream }).on('line', (line) => { + appendFileSync(recording, JSON.stringify({ direction, message: JSON.parse(line) }) + '\n'); + }); +} +process.on('SIGTERM', () => child.kill('SIGTERM')); +child.on('error', (error) => { + console.error(error); + process.exitCode = 1; +}); +child.on('close', (code) => { + process.stdin.destroy(); + process.exitCode = code === null ? 1 : code; +}); diff --git a/packages/gitlab-mcp/tests/manual/fixtures/scripted-gemini.mjs b/packages/gitlab-mcp/tests/manual/fixtures/scripted-gemini.mjs new file mode 100644 index 00000000..ffe806ab --- /dev/null +++ b/packages/gitlab-mcp/tests/manual/fixtures/scripted-gemini.mjs @@ -0,0 +1,68 @@ +import assert from 'node:assert/strict'; +import { createServer } from 'node:http'; + +/** Scripted provider responses drive the real CLI without credentials or model inference. */ +export async function startScriptedGemini(call) { + const requests = []; + let invoked = false; + let failure; + const server = createServer(async (request, response) => { + try { + assert.equal(request.method, 'POST'); + assert.ok(request.url.endsWith(':streamGenerateContent?alt=sse')); + assert.ok(requests.length < 16, 'Unexpected provider request loop'); + const chunks = []; + let size = 0; + for await (const chunk of request) { + size += chunk.length; + assert.ok(size <= 8 * 1024 * 1024, 'Provider request exceeds fixture limit'); + chunks.push(chunk); + } + const body = JSON.parse(Buffer.concat(chunks).toString('utf8')); + requests.push({ path: request.url, body }); + let parts = [{ text: 'Qualification complete.' }]; + // Auxiliary title/summary requests have no tools. Only the actual MCP dispatch is scripted. + if (body.tools && !invoked) { + const declarations = body.tools.flatMap(({ functionDeclarations }) => functionDeclarations); + assert.ok(declarations.some(({ name }) => name === 'call_mcp_tool')); + invoked = true; + parts = [ + { + functionCall: { + name: 'call_mcp_tool', + args: { + ServerName: 'gitlab', + ToolName: call.name, + Arguments: call.arguments, + }, + }, + }, + ]; + } + response.writeHead(200, { 'content-type': 'text/event-stream' }); + response.end( + `data: ${JSON.stringify({ + candidates: [{ content: { role: 'model', parts }, finishReason: 'STOP' }], + usageMetadata: { promptTokenCount: 1, candidatesTokenCount: 1, totalTokenCount: 2 }, + })}\n\n`, + ); + } catch (error) { + failure = error; + response.writeHead(500); + response.end('Fixture rejected request'); + } + }); + await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); + return { + url: `http://127.0.0.1:${server.address().port}`, + requests, + verify() { + if (failure) throw failure; + assert.equal(invoked, true); + }, + close: () => + new Promise((resolve, reject) => + server.close((error) => (error ? reject(error) : resolve())), + ), + }; +} diff --git a/packages/gitlab-mcp/tests/manual/fixtures/workflow-gitlab.mjs b/packages/gitlab-mcp/tests/manual/fixtures/workflow-gitlab.mjs index ab3747b2..857747b4 100644 --- a/packages/gitlab-mcp/tests/manual/fixtures/workflow-gitlab.mjs +++ b/packages/gitlab-mcp/tests/manual/fixtures/workflow-gitlab.mjs @@ -2,7 +2,7 @@ import { createServer } from 'node:http'; import { parse, visit, Kind } from 'graphql'; /** Deterministic GitLab fixture for real-client evaluations; no live credentials. */ -export async function startWorkflowGitLab() { +export async function startWorkflowGitLab({ emptyMergeRequests = false } = {}) { const requests = []; const user = { id: 1, @@ -108,7 +108,7 @@ export async function startWorkflowGitLab() { } function projectRest(path, response, json) { - if (path.endsWith('/merge_requests')) return json([mr]); + if (path.endsWith('/merge_requests')) return json(emptyMergeRequests ? [] : [mr]); if (path.endsWith('/merge_requests/12/changes')) return json({ ...mr, diff --git a/packages/gitlab-mcp/tests/scripts/antigravity-results.test.mjs b/packages/gitlab-mcp/tests/scripts/antigravity-results.test.mjs new file mode 100644 index 00000000..b71a1c71 --- /dev/null +++ b/packages/gitlab-mcp/tests/scripts/antigravity-results.test.mjs @@ -0,0 +1,178 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { startWorkflowGitLab } from '../manual/fixtures/workflow-gitlab.mjs'; +import { + clientResult, + recordedCall, + validateEnvelope, +} from '../../scripts/antigravity-contract.mjs'; +import { runEvaluation } from '../../scripts/evaluation-process.mjs'; +import { startScriptedGemini } from '../manual/fixtures/scripted-gemini.mjs'; + +test('MR fixture can return an empty list without changing its single-MR default', async () => { + // Empty results must reach the actual client as [], rather than a substituted entity. + for (const emptyMergeRequests of [false, true]) { + const fixture = await startWorkflowGitLab({ emptyMergeRequests }); + try { + const response = await fetch(`${fixture.url}/api/v4/projects/71/merge_requests`); + assert.deepEqual(await response.json(), emptyMergeRequests ? [] : [fixture.mr]); + } finally { + await fixture.close(); + } + } +}); + +test('recording preserves metadata and correlates the advertised tool with the actual response', () => { + // A passing client exit alone must not qualify an absent, different or schema-invalid invocation. + const expected = { name: 'fixture', arguments: {} }; + const result = { + content: [{ type: 'text', text: '{"value":1}' }], + structuredContent: { value: 1 }, + _meta: { display: 'fixture' }, + }; + const tool = { + name: 'fixture', + inputSchema: { type: 'object' }, + outputSchema: { + type: 'object', + properties: { value: { type: 'integer' } }, + required: ['value'], + }, + }; + const records = [ + { + direction: 'request', + message: { + jsonrpc: '2.0', + id: 'call', + method: 'tools/call', + params: { ...expected, _meta: { progressToken: 'progress' } }, + }, + }, + { + direction: 'response', + message: { jsonrpc: '2.0', id: 'unrelated', result: { content: [] } }, + }, + { direction: 'response', message: { jsonrpc: '2.0', id: 'list', result: { tools: [tool] } } }, + { direction: 'response', message: { jsonrpc: '2.0', id: 'call', result } }, + ]; + const captured = recordedCall(records, expected); + assert.equal(captured.result, result); + assert.deepEqual(captured.request.params._meta, { progressToken: 'progress' }); + assert.throws(() => recordedCall(records, { ...expected, arguments: { different: true } })); + assert.throws(() => recordedCall(records.slice(0, 3), expected), /Missing MCP tool result/); + assert.throws(() => recordedCall(records.slice(1), expected), /exactly one/); + assert.throws(() => validateEnvelope({ ...result, structuredContent: { value: 'wrong' } }, tool)); + assert.throws(() => validateEnvelope({ content: [] }, tool), /Missing required/); + assert.throws(() => validateEnvelope({ content: [], structuredContent: [] }, tool)); + validateEnvelope({ content: [{ type: 'text', text: 'denied' }], isError: true }, tool); +}); + +test('client environment is isolated from inherited credentials', async () => { + // Passing a clean environment must reach the client instead of silently inheriting the caller. + const result = await runEvaluation( + process.execPath, + [ + '-e', + 'console.log(JSON.stringify({ marker: process.env.QUALIFICATION_MARKER, gitlab: Object.hasOwn(process.env,"GITLAB_TOKEN") }))', + ], + { + cwd: process.cwd(), + env: { QUALIFICATION_MARKER: 'fixture-only' }, + timeoutMs: 30000, + }, + ); + assert.equal(result.code, 0, result.stderr); + assert.deepEqual(JSON.parse(result.stdout), { marker: 'fixture-only', gitlab: false }); +}); + +test('client qualification rejects changed payloads, lost errors and missing provider responses', () => { + // A tool transcript must agree with the wire payload and with the next provider request. + const call = { name: 'fixture', arguments: {} }; + for (const isError of [false, true]) { + const text = isError ? '{"error":"denied"}' : '[]'; + const info = { + name: 'call_mcp_tool', + parameters: { ServerName: 'gitlab', ToolName: call.name, Arguments: call.arguments }, + output: text, + }; + if (isError) info.error = { type: 'TOOL_ERROR', message: text }; + const events = [ + { + step_update: { + step_type: 'tool', + state: isError ? 'ERROR' : 'DONE', + tool_name: 'call_mcp_tool', + tool_info: info, + }, + }, + { event: 'result', result: { status: 'SUCCESS' } }, + ]; + const key = isError ? 'error' : 'output'; + const response = { + [key]: + 'Created At: 2026-10-30T00:00:00Z\nCompleted At: 2026-10-30T00:00:01Z\n' + + (isError ? 'Encountered error in tool execution: ' : '') + + text, + }; + const requests = [ + { + body: { + contents: [{ parts: [{ functionResponse: { name: 'call_mcp_tool', response } }] }], + }, + }, + ]; + assert.deepEqual(clientResult(events, requests, call, text, isError), { + decoded: info, + providerResponse: response, + }); + assert.throws(() => clientResult(events, requests, call, '{}', isError), /changed the result/); + assert.throws(() => clientResult(events, [], call, text, isError), /provider tool response/); + assert.throws(() => clientResult(events, requests, call, text, !isError)); + const changed = globalThis.structuredClone(requests); + changed[0].body.contents[0].parts[0].functionResponse.response[key] += 'changed'; + assert.throws(() => clientResult(events, changed, call, text, isError)); + } +}); + +test('scripted provider leaves auxiliary requests alone and emits only the selected MCP call', async () => { + // AGY may generate a title first; that must not consume or replace the measured MCP invocation. + const call = { + name: 'browse_merge_requests', + arguments: { action: 'list', project_id: 'test/backend' }, + }; + const provider = await startScriptedGemini(call); + const send = async (body) => { + const response = await fetch( + `${provider.url}/v1beta/models/fixture:streamGenerateContent?alt=sse`, + { + method: 'POST', + body: JSON.stringify(body), + }, + ); + assert.equal(response.status, 200); + const text = await response.text(); + assert.ok(text.startsWith('data: ') && text.endsWith('\n\n')); + return JSON.parse(text.slice(6, -2)).candidates[0].content.parts; + }; + try { + assert.deepEqual(await send({ contents: [] }), [{ text: 'Qualification complete.' }]); + assert.throws(() => provider.verify()); + assert.deepEqual( + await send({ tools: [{ functionDeclarations: [{ name: 'call_mcp_tool' }] }] }), + [ + { + functionCall: { + name: 'call_mcp_tool', + args: { ServerName: 'gitlab', ToolName: call.name, Arguments: call.arguments }, + }, + }, + ], + ); + assert.deepEqual(await send({ contents: [] }), [{ text: 'Qualification complete.' }]); + provider.verify(); + assert.equal(provider.requests.length, 3); + } finally { + await provider.close(); + } +}); diff --git a/packages/gitlab-mcp/tests/unit/evaluation.test.ts b/packages/gitlab-mcp/tests/unit/evaluation.test.ts index f718f17f..35262aac 100644 --- a/packages/gitlab-mcp/tests/unit/evaluation.test.ts +++ b/packages/gitlab-mcp/tests/unit/evaluation.test.ts @@ -1,12 +1,16 @@ import { spawnSync } from 'node:child_process'; import { resolve } from 'node:path'; -it('verifies the installed-workflow evaluator and its real loopback fixture', () => { +it('verifies installed-client evaluators and their loopback fixtures', () => { // Run ESM helpers in their native Node runtime rather than a CommonJS Jest transform. - const result = spawnSync(process.execPath, ['--test', 'tests/scripts/evaluation.test.mjs'], { - cwd: resolve(__dirname, '../..'), - encoding: 'utf8', - }); + const result = spawnSync( + process.execPath, + ['--test', 'tests/scripts/evaluation.test.mjs', 'tests/scripts/antigravity-results.test.mjs'], + { + cwd: resolve(__dirname, '../..'), + encoding: 'utf8', + }, + ); // The node:test report names the failing case; keep it in the failure message. expect({ status: result.status, report: result.stdout + result.stderr }).toMatchObject({ status: 0, From 801ef5dabbe1998ed80277735880f179e274974a Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Fri, 30 Oct 2026 00:36:42 +0200 Subject: [PATCH 2/3] test(mcp): retain setup failure reports --- .../gitlab-mcp/scripts/qualify-antigravity.mjs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/gitlab-mcp/scripts/qualify-antigravity.mjs b/packages/gitlab-mcp/scripts/qualify-antigravity.mjs index b5aa1812..d9fe008d 100644 --- a/packages/gitlab-mcp/scripts/qualify-antigravity.mjs +++ b/packages/gitlab-mcp/scripts/qualify-antigravity.mjs @@ -282,18 +282,18 @@ async function main() { status: 'failed', nodeVersion: process.version, platform: process.platform, - sourceCommit: launchSync(resolveExecutable('git'), ['rev-parse', 'HEAD'], { + scenarios: [], + }; + try { + report.sourceCommit = launchSync(resolveExecutable('git'), ['rev-parse', 'HEAD'], { cwd: root, encoding: 'utf8', - }).trim(), - sourceDirty: + }).trim(); + report.sourceDirty = launchSync(resolveExecutable('git'), ['status', '--porcelain'], { cwd: root, encoding: 'utf8', - }).length > 0, - scenarios: [], - }; - try { + }).length > 0; const prepared = await prepare(runtime, archive); report.client = { version: prepared.manifest.version, From 3ef1565697dcc1300af7d29801fe1c115a0d3332 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Fri, 30 Oct 2026 09:29:05 +0200 Subject: [PATCH 3/3] docs(readme): list supported MCP clients --- README.md | 29 +++++++++++++++++++++++++++-- packages/gitlab-mcp-db/README.md | 29 +++++++++++++++++++++++++++-- packages/gitlab-mcp/README.md | 27 ++++++++++++++++++++++++++- packages/gitlab-mcp/README.md.in | 27 ++++++++++++++++++++++++++- 4 files changed, 106 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 1a687ad0..cb213e0a 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![npm version](https://img.shields.io/npm/v/@structured-world/gitlab-mcp)](https://www.npmjs.com/package/@structured-world/gitlab-mcp) [![npm downloads](https://img.shields.io/npm/dm/@structured-world/gitlab-mcp)](https://www.npmjs.com/package/@structured-world/gitlab-mcp) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](./LICENSE) [![Release](https://github.com/structured-world/gitlab-mcp/workflows/Release/badge.svg)](https://github.com/structured-world/gitlab-mcp/actions) [![Coverage](https://codecov.io/gh/structured-world/gitlab-mcp/graph/badge.svg)](https://codecov.io/gh/structured-world/gitlab-mcp) [![Coverage Report](https://img.shields.io/badge/Coverage-Live%20Report-brightgreen?logo=github)](https://gitlab-mcp.sw.foundation/coverage/) -Advanced GitLab MCP server — 64 CQRS tools exposing 234 GitLab operations across 26 entity types. The tool catalog and parameters are filtered to each instance's GitLab version, tier, and token scopes, so the agent sees only what the connected instance actually supports. +Advanced GitLab MCP server: 64 CQRS tools exposing 235 GitLab operations across 26 entity types. The tool catalog and parameters are filtered to each instance's GitLab version, tier, and token scopes, so the agent sees only what the connected instance actually supports. [![Install in Claude Desktop](https://img.shields.io/badge/Claude_Desktop-Install_Extension-F97316?style=for-the-badge)](https://gitlab-mcp.sw.foundation/downloads/gitlab-mcp-12.0.3.mcpb) [![Install in VS Code](https://img.shields.io/badge/VS_Code-Install_MCP_Server-007ACC?style=for-the-badge&logo=visualstudiocode&logoColor=white)](vscode:mcp/install?%7B%22name%22%3A%22gitlab-mcp%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40structured-world%2Fgitlab-mcp%22%5D%7D) @@ -34,6 +34,31 @@ This is a monorepo with two npm packages: **Requirements:** Node.js >= 24 +## Supported clients + +| Client | Setup | +|--------|-------| +| **Codex (CLI and desktop)** | [Local MCP registration or remote HTTP server](https://gitlab-mcp.sw.foundation/guide/installation/codex) | +| **Antigravity CLI (`agy`)** | [Workspace MCP configuration and tested compatibility](https://gitlab-mcp.sw.foundation/clients/antigravity) | +| Claude Desktop | [Desktop extension or MCP configuration](https://gitlab-mcp.sw.foundation/clients/claude-desktop) | +| Claude Code | [CLI or MCP configuration](https://gitlab-mcp.sw.foundation/clients/claude-code) | +| VS Code | [MCP configuration](https://gitlab-mcp.sw.foundation/clients/vscode) | +| Cursor | [MCP configuration](https://gitlab-mcp.sw.foundation/clients/cursor) | +| Windsurf | [MCP configuration](https://gitlab-mcp.sw.foundation/clients/windsurf) | +| Cline | [MCP configuration](https://gitlab-mcp.sw.foundation/clients/cline) | +| Roo Code | [MCP configuration](https://gitlab-mcp.sw.foundation/clients/roo-code) | + +With the Codex CLI installed and `GITLAB_API_URL` and `GITLAB_TOKEN` set in your +environment, run `npx @structured-world/gitlab-mcp install --codex` to register a +local server. For Antigravity, add the server +to `.agents/mcp_config.json` in your workspace. Follow the linked guides for +authentication, remote-server setup and client-specific configuration. + +Codex and Claude Code can be configured side by side. Antigravity CLI 1.4.10 is +qualified against the installed core package on macOS and Linux, including empty +and single-MR lists. See the [client overview](https://gitlab-mcp.sw.foundation/clients/) +for all setup guides. + ## Highlights - **64 tools** across 26 entity types — projects, merge requests, pipelines, work items, wiki, and more @@ -52,7 +77,7 @@ This is a monorepo with two npm packages: GitLab exposes hundreds of API operations. Giving each its own MCP tool floods an agent's context; a thin API wrapper pushes that complexity back onto the agent. This server takes a third path: 64 CQRS tools, each holding several typed -actions, expose 234 operations across 26 entity types. +actions, expose 235 operations across 26 entity types. - `browse_*` tools are read-only queries; `manage_*` tools are writes. - Sub-resources fold into actions rather than new tools — `browse_pipelines` covers diff --git a/packages/gitlab-mcp-db/README.md b/packages/gitlab-mcp-db/README.md index eb54ed45..1b2213fb 100644 --- a/packages/gitlab-mcp-db/README.md +++ b/packages/gitlab-mcp-db/README.md @@ -2,7 +2,7 @@ [![npm version](https://img.shields.io/npm/v/@structured-world/gitlab-mcp)](https://www.npmjs.com/package/@structured-world/gitlab-mcp) [![npm downloads](https://img.shields.io/npm/dm/@structured-world/gitlab-mcp)](https://www.npmjs.com/package/@structured-world/gitlab-mcp) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/structured-world/gitlab-mcp/blob/HEAD/LICENSE) [![Release](https://github.com/structured-world/gitlab-mcp/workflows/Release/badge.svg)](https://github.com/structured-world/gitlab-mcp/actions) [![Coverage](https://codecov.io/gh/structured-world/gitlab-mcp/graph/badge.svg)](https://codecov.io/gh/structured-world/gitlab-mcp) [![Coverage Report](https://img.shields.io/badge/Coverage-Live%20Report-brightgreen?logo=github)](https://gitlab-mcp.sw.foundation/coverage/) -Advanced GitLab MCP server — 64 CQRS tools exposing 234 GitLab operations across 26 entity types. The tool catalog and parameters are filtered to each instance's GitLab version, tier, and token scopes, so the agent sees only what the connected instance actually supports. +Advanced GitLab MCP server: 64 CQRS tools exposing 235 GitLab operations across 26 entity types. The tool catalog and parameters are filtered to each instance's GitLab version, tier, and token scopes, so the agent sees only what the connected instance actually supports. [![Install in Claude Desktop](https://img.shields.io/badge/Claude_Desktop-Install_Extension-F97316?style=for-the-badge)](https://gitlab-mcp.sw.foundation/downloads/gitlab-mcp-12.0.3.mcpb) [![Install in VS Code](https://img.shields.io/badge/VS_Code-Install_MCP_Server-007ACC?style=for-the-badge&logo=visualstudiocode&logoColor=white)](vscode:mcp/install?%7B%22name%22%3A%22gitlab-mcp%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40structured-world%2Fgitlab-mcp%22%5D%7D) @@ -34,6 +34,31 @@ This is a monorepo with two npm packages: **Requirements:** Node.js >= 24 +## Supported clients + +| Client | Setup | +|--------|-------| +| **Codex (CLI and desktop)** | [Local MCP registration or remote HTTP server](https://gitlab-mcp.sw.foundation/guide/installation/codex) | +| **Antigravity CLI (`agy`)** | [Workspace MCP configuration and tested compatibility](https://gitlab-mcp.sw.foundation/clients/antigravity) | +| Claude Desktop | [Desktop extension or MCP configuration](https://gitlab-mcp.sw.foundation/clients/claude-desktop) | +| Claude Code | [CLI or MCP configuration](https://gitlab-mcp.sw.foundation/clients/claude-code) | +| VS Code | [MCP configuration](https://gitlab-mcp.sw.foundation/clients/vscode) | +| Cursor | [MCP configuration](https://gitlab-mcp.sw.foundation/clients/cursor) | +| Windsurf | [MCP configuration](https://gitlab-mcp.sw.foundation/clients/windsurf) | +| Cline | [MCP configuration](https://gitlab-mcp.sw.foundation/clients/cline) | +| Roo Code | [MCP configuration](https://gitlab-mcp.sw.foundation/clients/roo-code) | + +With the Codex CLI installed and `GITLAB_API_URL` and `GITLAB_TOKEN` set in your +environment, run `npx @structured-world/gitlab-mcp install --codex` to register a +local server. For Antigravity, add the server +to `.agents/mcp_config.json` in your workspace. Follow the linked guides for +authentication, remote-server setup and client-specific configuration. + +Codex and Claude Code can be configured side by side. Antigravity CLI 1.4.10 is +qualified against the installed core package on macOS and Linux, including empty +and single-MR lists. See the [client overview](https://gitlab-mcp.sw.foundation/clients/) +for all setup guides. + ## Highlights - **64 tools** across 26 entity types — projects, merge requests, pipelines, work items, wiki, and more @@ -52,7 +77,7 @@ This is a monorepo with two npm packages: GitLab exposes hundreds of API operations. Giving each its own MCP tool floods an agent's context; a thin API wrapper pushes that complexity back onto the agent. This server takes a third path: 64 CQRS tools, each holding several typed -actions, expose 234 operations across 26 entity types. +actions, expose 235 operations across 26 entity types. - `browse_*` tools are read-only queries; `manage_*` tools are writes. - Sub-resources fold into actions rather than new tools — `browse_pipelines` covers diff --git a/packages/gitlab-mcp/README.md b/packages/gitlab-mcp/README.md index 9569a4ff..1b2213fb 100644 --- a/packages/gitlab-mcp/README.md +++ b/packages/gitlab-mcp/README.md @@ -2,7 +2,7 @@ [![npm version](https://img.shields.io/npm/v/@structured-world/gitlab-mcp)](https://www.npmjs.com/package/@structured-world/gitlab-mcp) [![npm downloads](https://img.shields.io/npm/dm/@structured-world/gitlab-mcp)](https://www.npmjs.com/package/@structured-world/gitlab-mcp) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/structured-world/gitlab-mcp/blob/HEAD/LICENSE) [![Release](https://github.com/structured-world/gitlab-mcp/workflows/Release/badge.svg)](https://github.com/structured-world/gitlab-mcp/actions) [![Coverage](https://codecov.io/gh/structured-world/gitlab-mcp/graph/badge.svg)](https://codecov.io/gh/structured-world/gitlab-mcp) [![Coverage Report](https://img.shields.io/badge/Coverage-Live%20Report-brightgreen?logo=github)](https://gitlab-mcp.sw.foundation/coverage/) -Advanced GitLab MCP server — 64 CQRS tools exposing 235 GitLab operations across 26 entity types. The tool catalog and parameters are filtered to each instance's GitLab version, tier, and token scopes, so the agent sees only what the connected instance actually supports. +Advanced GitLab MCP server: 64 CQRS tools exposing 235 GitLab operations across 26 entity types. The tool catalog and parameters are filtered to each instance's GitLab version, tier, and token scopes, so the agent sees only what the connected instance actually supports. [![Install in Claude Desktop](https://img.shields.io/badge/Claude_Desktop-Install_Extension-F97316?style=for-the-badge)](https://gitlab-mcp.sw.foundation/downloads/gitlab-mcp-12.0.3.mcpb) [![Install in VS Code](https://img.shields.io/badge/VS_Code-Install_MCP_Server-007ACC?style=for-the-badge&logo=visualstudiocode&logoColor=white)](vscode:mcp/install?%7B%22name%22%3A%22gitlab-mcp%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40structured-world%2Fgitlab-mcp%22%5D%7D) @@ -34,6 +34,31 @@ This is a monorepo with two npm packages: **Requirements:** Node.js >= 24 +## Supported clients + +| Client | Setup | +|--------|-------| +| **Codex (CLI and desktop)** | [Local MCP registration or remote HTTP server](https://gitlab-mcp.sw.foundation/guide/installation/codex) | +| **Antigravity CLI (`agy`)** | [Workspace MCP configuration and tested compatibility](https://gitlab-mcp.sw.foundation/clients/antigravity) | +| Claude Desktop | [Desktop extension or MCP configuration](https://gitlab-mcp.sw.foundation/clients/claude-desktop) | +| Claude Code | [CLI or MCP configuration](https://gitlab-mcp.sw.foundation/clients/claude-code) | +| VS Code | [MCP configuration](https://gitlab-mcp.sw.foundation/clients/vscode) | +| Cursor | [MCP configuration](https://gitlab-mcp.sw.foundation/clients/cursor) | +| Windsurf | [MCP configuration](https://gitlab-mcp.sw.foundation/clients/windsurf) | +| Cline | [MCP configuration](https://gitlab-mcp.sw.foundation/clients/cline) | +| Roo Code | [MCP configuration](https://gitlab-mcp.sw.foundation/clients/roo-code) | + +With the Codex CLI installed and `GITLAB_API_URL` and `GITLAB_TOKEN` set in your +environment, run `npx @structured-world/gitlab-mcp install --codex` to register a +local server. For Antigravity, add the server +to `.agents/mcp_config.json` in your workspace. Follow the linked guides for +authentication, remote-server setup and client-specific configuration. + +Codex and Claude Code can be configured side by side. Antigravity CLI 1.4.10 is +qualified against the installed core package on macOS and Linux, including empty +and single-MR lists. See the [client overview](https://gitlab-mcp.sw.foundation/clients/) +for all setup guides. + ## Highlights - **64 tools** across 26 entity types — projects, merge requests, pipelines, work items, wiki, and more diff --git a/packages/gitlab-mcp/README.md.in b/packages/gitlab-mcp/README.md.in index 365d8866..66c78ac3 100644 --- a/packages/gitlab-mcp/README.md.in +++ b/packages/gitlab-mcp/README.md.in @@ -2,7 +2,7 @@ [![npm version](https://img.shields.io/npm/v/@structured-world/gitlab-mcp)](https://www.npmjs.com/package/@structured-world/gitlab-mcp) [![npm downloads](https://img.shields.io/npm/dm/@structured-world/gitlab-mcp)](https://www.npmjs.com/package/@structured-world/gitlab-mcp) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](__REPO_BASE__/LICENSE) [![Release](https://github.com/structured-world/gitlab-mcp/workflows/Release/badge.svg)](https://github.com/structured-world/gitlab-mcp/actions) [![Coverage](https://codecov.io/gh/structured-world/gitlab-mcp/graph/badge.svg)](https://codecov.io/gh/structured-world/gitlab-mcp) [![Coverage Report](https://img.shields.io/badge/Coverage-Live%20Report-brightgreen?logo=github)](https://gitlab-mcp.sw.foundation/coverage/) -Advanced GitLab MCP server — __TOOL_COUNT__ CQRS tools exposing __ACTION_COUNT__ GitLab operations across __ENTITY_COUNT__ entity types. The tool catalog and parameters are filtered to each instance's GitLab version, tier, and token scopes, so the agent sees only what the connected instance actually supports. +Advanced GitLab MCP server: __TOOL_COUNT__ CQRS tools exposing __ACTION_COUNT__ GitLab operations across __ENTITY_COUNT__ entity types. The tool catalog and parameters are filtered to each instance's GitLab version, tier, and token scopes, so the agent sees only what the connected instance actually supports. [![Install in Claude Desktop](https://img.shields.io/badge/Claude_Desktop-Install_Extension-F97316?style=for-the-badge)](https://gitlab-mcp.sw.foundation/downloads/gitlab-mcp-__VERSION__.mcpb) [![Install in VS Code](https://img.shields.io/badge/VS_Code-Install_MCP_Server-007ACC?style=for-the-badge&logo=visualstudiocode&logoColor=white)](vscode:mcp/install?%7B%22name%22%3A%22gitlab-mcp%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40structured-world%2Fgitlab-mcp%22%5D%7D) @@ -34,6 +34,31 @@ This is a monorepo with two npm packages: **Requirements:** Node.js >= 24 +## Supported clients + +| Client | Setup | +|--------|-------| +| **Codex (CLI and desktop)** | [Local MCP registration or remote HTTP server](https://gitlab-mcp.sw.foundation/guide/installation/codex) | +| **Antigravity CLI (`agy`)** | [Workspace MCP configuration and tested compatibility](https://gitlab-mcp.sw.foundation/clients/antigravity) | +| Claude Desktop | [Desktop extension or MCP configuration](https://gitlab-mcp.sw.foundation/clients/claude-desktop) | +| Claude Code | [CLI or MCP configuration](https://gitlab-mcp.sw.foundation/clients/claude-code) | +| VS Code | [MCP configuration](https://gitlab-mcp.sw.foundation/clients/vscode) | +| Cursor | [MCP configuration](https://gitlab-mcp.sw.foundation/clients/cursor) | +| Windsurf | [MCP configuration](https://gitlab-mcp.sw.foundation/clients/windsurf) | +| Cline | [MCP configuration](https://gitlab-mcp.sw.foundation/clients/cline) | +| Roo Code | [MCP configuration](https://gitlab-mcp.sw.foundation/clients/roo-code) | + +With the Codex CLI installed and `GITLAB_API_URL` and `GITLAB_TOKEN` set in your +environment, run `npx @structured-world/gitlab-mcp install --codex` to register a +local server. For Antigravity, add the server +to `.agents/mcp_config.json` in your workspace. Follow the linked guides for +authentication, remote-server setup and client-specific configuration. + +Codex and Claude Code can be configured side by side. Antigravity CLI 1.4.10 is +qualified against the installed core package on macOS and Linux, including empty +and single-MR lists. See the [client overview](https://gitlab-mcp.sw.foundation/clients/) +for all setup guides. + ## Highlights - **__TOOL_COUNT__ tools** across __ENTITY_COUNT__ entity types — projects, merge requests, pipelines, work items, wiki, and more