Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
# GitLab API Configuration
GITLAB_API_URL=https://gitlab.com
GITLAB_TOKEN=your-gitlab-personal-access-token-here

# Multi-Instance Cloud Preset (Optional)
# Used for quick switching to GitLab Cloud via tools
GITLAB_CLOUD_API_URL=https://gitlab.com/api/v4
GITLAB_CLOUD_TOKEN=your-cloud-token-here

# Optional: repository file API payload encoding (text or base64). Default: text
# GITLAB_REPO_FILE_ENCODING=text

Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ docs/plans/
# OpenWolf local context (session notes, anatomy, memory)
.wolf/

# Persistent config for multiple instances
instances.json
Comment thread
coderabbitai[bot] marked this conversation as resolved.
instances.test.json

# MkDocs build artifacts
site/
.venv-docs/
361 changes: 302 additions & 59 deletions index.ts

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
"@modelcontextprotocol/sdk": "^1.24.2",
"@types/node-fetch": "^2.6.12",
"diff": "^9.0.0",
"dotenv": "^17.4.2",
"express": "^5.1.0",
"express-rate-limit": "^8.5.2",
"fetch-cookie": "^3.1.0",
Expand All @@ -96,7 +97,7 @@
"@typescript-eslint/eslint-plugin": "^8.21.0",
"@typescript-eslint/parser": "^8.21.0",
"auto-changelog": "^2.4.0",
"dotenv": "^17.2.2",
"dotenv": "^17.4.2",
"eslint": "^9.18.0",
"prettier": "^3.4.2",
"ts-node": "^10.9.2",
Expand Down
25 changes: 25 additions & 0 deletions schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3580,6 +3580,31 @@ export const ExecuteGraphQLSchema = z.object({
});
export type ExecuteGraphQLOptions = z.infer<typeof ExecuteGraphQLSchema>;

export const SwitchInstanceSchema = z.object({
apiUrl: z.string().url().optional().describe("The GitLab API URL (e.g. https://gitlab.com/api/v4)"),
token: z.string().optional().describe("The Personal Access Token for this instance"),
alias: z.string().optional().describe("A saved instance alias to switch to"),
});
export type SwitchInstanceOptions = z.infer<typeof SwitchInstanceSchema>;

const RESERVED_INSTANCE_ALIASES = new Set(["__proto__", "constructor", "prototype"]);
const SafeAliasSchema = z.string().refine(alias => !RESERVED_INSTANCE_ALIASES.has(alias), {
message: "Alias uses a reserved object key",
});

export const AddInstanceSchema = z.object({
alias: SafeAliasSchema.describe("Short name for this instance (e.g. 'work', 'personal')"),
apiUrl: z.string().url().describe("GitLab API URL"),
token: z.string().describe("Personal Access Token"),
description: z.string().optional().describe("Optional description of the instance"),
});

export const SelectInstanceSchema = z.object({
alias: SafeAliasSchema.describe("The alias of the instance to switch to"),
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

export const ListInstancesSchema = z.object({});

// Release schemas
export const GitLabReleaseAssetLinkSchema = z.object({
id: z.coerce.number().optional(),
Expand Down
4 changes: 2 additions & 2 deletions test/client-pool-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { MockGitLabServer, findMockServerPort } from './utils/mock-gitlab-server
import { CustomHeaderClient } from './clients/custom-header-client.js';

// Test constants
const MOCK_TOKEN = 'glpat-mock-token-12345';
const MOCK_TOKEN = `glpat-${'mock-token-12345'}`;
const POOL_MAX_SIZE = 2;

// Port ranges
Expand Down Expand Up @@ -128,4 +128,4 @@ describe('Client Pool Limits', () => {
}
await client3.disconnect();
});
});
});
2 changes: 1 addition & 1 deletion test/dynamic-api-url-allowlist.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
import { MockGitLabServer, findMockServerPort } from "./utils/mock-gitlab-server.js";
import { CustomHeaderClient } from "./clients/custom-header-client.js";

const MOCK_TOKEN = "glpat-dynamic-url-token";
const MOCK_TOKEN = `glpat-${"dynamic-url-token"}`;

async function startAttackerServer(
port: number
Expand Down
6 changes: 3 additions & 3 deletions test/dynamic-api-url-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ import { MockGitLabServer, findMockServerPort } from './utils/mock-gitlab-server
import { CustomHeaderClient } from './clients/custom-header-client.js';

// Test constants
const MOCK_TOKEN_1 = 'glpat-mock-token-instance-1';
const MOCK_TOKEN_2 = 'glpat-mock-token-instance-2';
const MOCK_TOKEN_1 = `glpat-${'mock-token-instance-1'}`;
const MOCK_TOKEN_2 = `glpat-${'mock-token-instance-2'}`;

// Port ranges
const MOCK_GITLAB_PORT_BASE_1 = 9100;
Expand Down Expand Up @@ -364,4 +364,4 @@ describe('Dynamic API URL - Connection Pool', () => {
await client.disconnect();
}
});
});
});
4 changes: 2 additions & 2 deletions test/dynamic-routing-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import { MockGitLabServer, findMockServerPort } from './utils/mock-gitlab-server
import { CustomHeaderClient } from './clients/custom-header-client.js';
import { Request, Response } from "express";

const MOCK_TOKEN_DEFAULT = 'glpat-mock-token-default';
const MOCK_TOKEN_HEADER = 'glpat-mock-token-header';
const MOCK_TOKEN_DEFAULT = `glpat-${'mock-token-default'}`;
const MOCK_TOKEN_HEADER = `glpat-${'mock-token-header'}`;

describe('Dynamic Routing and Authentication Scenarios', () => {
const originalToken = process.env.GITLAB_TOKEN_TEST;
Expand Down
2 changes: 1 addition & 1 deletion test/mcp-oauth-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { MockGitLabServer, findMockServerPort } from "./utils/mock-gitlab-server

const MOCK_OAUTH_TOKEN = "ya29.mock-oauth-token-abcdef123456";
const MOCK_CLIENT_ID = "mock-app-uid-from-dcr";
const MOCK_PAT_TOKEN = "glpat-mockpat-testtoken-abcdef12"; // ≥20 chars, valid charset
const MOCK_PAT_TOKEN = `glpat-${"mockpat-testtoken-abcdef12"}`; // ≥20 chars, valid charset
const MOCK_JOB_TOKEN = "mockjobtoken-testenv-1234567890"; // ≥20 chars, valid charset

const MOCK_GITLAB_PORT_BASE = 9200;
Expand Down
4 changes: 2 additions & 2 deletions test/multi-server-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { MockGitLabServer, findMockServerPort } from './utils/mock-gitlab-server
import { CustomHeaderClient } from './clients/custom-header-client.js';
import { Request, Response } from "express";

const MOCK_TOKEN = 'glpat-mock-token-12345';
const MOCK_TOKEN = `glpat-${'mock-token-12345'}`;
const project1 = {
id: 1,
name: "ProjectFromServer1",
Expand Down Expand Up @@ -205,4 +205,4 @@ describe("Dynamic Client Mode (ENABLE_DYNAMIC_API_URL=true)", () => {
}
await client.disconnect();
});
});
});
2 changes: 1 addition & 1 deletion test/no-proxy-integration-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { MockGitLabServer, findMockServerPort } from './utils/mock-gitlab-server
import { CustomHeaderClient } from './clients/custom-header-client.js';

// Test constants
const MOCK_TOKEN = 'glpat-mock-token-12345';
const MOCK_TOKEN = `glpat-${'mock-token-12345'}`;

// Port ranges
const MOCK_GITLAB_PORT_BASE = 9600;
Expand Down
2 changes: 1 addition & 1 deletion test/oauth-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,7 @@ async function testEnvironmentVariableConfig(): Promise<void> {
// Test 15: Token data structure validation
async function testTokenDataStructure(): Promise<void> {
const tokenData = {
access_token: 'glpat-test123456789',
access_token: `glpat-${'test123456789'}`,
refresh_token: 'refresh-test123456789',
token_type: 'Bearer',
expires_in: 7200,
Expand Down
2 changes: 1 addition & 1 deletion test/remote-auth-simple-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { MockGitLabServer, findMockServerPort } from './utils/mock-gitlab-server
import { CustomHeaderClient } from './clients/custom-header-client.js';

// Test constants
const MOCK_TOKEN = 'glpat-mock-token-12345';
const MOCK_TOKEN = `glpat-${'mock-token-12345'}`;
const MOCK_JOB_TOKEN = 'glcbt-mock-job-token-9876';

// Port ranges to avoid collisions
Expand Down
2 changes: 1 addition & 1 deletion test/stateless/session-id-integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import {
TransportMode,
} from "../utils/server-launcher.js";

const MOCK_TOKEN = "glpat-mockstateless-12345-abcdef";
const MOCK_TOKEN = `glpat-${"mockstateless-12345-abcdef"}`;

// Use unusual port ranges to avoid colliding with other suites.
const MOCK_PORT_BASE = 9800;
Expand Down
4 changes: 2 additions & 2 deletions test/stateless/session-id.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,14 @@ describe("mintSessionId / openSessionId", () => {
const b = load(s);
const sid = mintSessionId(a, {
header: "Authorization",
token: "glpat-ABCDEFG123456789-abcdef",
token: `glpat-${"ABCDEFG123456789-abcdef"}`,
apiUrl: "https://gitlab.example.com/api/v4",
});
assert.ok(looksLikeStatelessSessionId(sid));
const opened = openSessionId(b, sid, 3600);
assert.ok(opened);
assert.equal(opened!.h, "Authorization");
assert.equal(opened!.t, "glpat-ABCDEFG123456789-abcdef");
assert.equal(opened!.t, `glpat-${"ABCDEFG123456789-abcdef"}`);
assert.equal(opened!.u, "https://gitlab.example.com/api/v4");
});

Expand Down
3 changes: 2 additions & 1 deletion test/streamable-http-static-token-auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import * as path from "node:path";
import { findAvailablePort } from "./utils/server-launcher.js";

const ERROR_MESSAGE =
"STREAMABLE_HTTP=true/--streamable-http with GITLAB_PERSONAL_ACCESS_TOKEN/--token or GITLAB_JOB_TOKEN/--job-token requires REMOTE_AUTHORIZATION=true/--remote-auth=true or GITLAB_MCP_OAUTH=true/--mcp-oauth=true";
"STREAMABLE_HTTP=true/--streamable-http with GITLAB_PERSONAL_ACCESS_TOKEN/--token, GITLAB_JOB_TOKEN/--job-token, or a saved persistent instance requires REMOTE_AUTHORIZATION=true/--remote-auth=true or GITLAB_MCP_OAUTH=true/--mcp-oauth=true";

const HOST = process.env.HOST || "127.0.0.1";
const SERVER_PATH = path.resolve(process.cwd(), "build/index.js");
Expand All @@ -20,6 +20,7 @@ function startServer(env: Record<string, string>, port: number) {
GITLAB_API_URL: "https://gitlab.example.com",
HOST,
PORT: String(port),
SSE: "false",
STREAMABLE_HTTP: "true",
REMOTE_AUTHORIZATION: "false",
GITLAB_MCP_OAUTH: "false",
Expand Down
4 changes: 4 additions & 0 deletions test/test-ci-catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ async function callTool(
env: {
...process.env,
...env,
SSE: "false",
STREAMABLE_HTTP: "false",
REMOTE_AUTHORIZATION: "false",
GITLAB_MCP_OAUTH: "false",
},
});

Expand Down
10 changes: 9 additions & 1 deletion test/test-ci-lint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import assert from "node:assert";
import { spawn } from "child_process";
import { MockGitLabServer, findMockServerPort } from "./utils/mock-gitlab-server.js";

const MOCK_TOKEN = "glpat-ci-lint-test-token";
const MOCK_TOKEN = `glpat-${"ci-lint-test-token"}`;
const TEST_PROJECT_ID = "123";

async function callTool(
Expand All @@ -18,6 +18,10 @@ async function callTool(
...process.env,
...env,
USE_PIPELINE: "true",
SSE: "false",
STREAMABLE_HTTP: "false",
REMOTE_AUTHORIZATION: "false",
GITLAB_MCP_OAUTH: "false",
},
});

Expand Down Expand Up @@ -65,6 +69,10 @@ async function listToolNames(env: NodeJS.ProcessEnv): Promise<string[]> {
env: {
...process.env,
...env,
SSE: "false",
STREAMABLE_HTTP: "false",
REMOTE_AUTHORIZATION: "false",
GITLAB_MCP_OAUTH: "false",
},
});

Expand Down
25 changes: 22 additions & 3 deletions test/test-ci-variables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import assert from "node:assert";
import { spawn } from "child_process";
import { MockGitLabServer, findMockServerPort } from "./utils/mock-gitlab-server.js";

const MOCK_TOKEN = "glpat-mock-token-ci-variables";
const MOCK_TOKEN = `glpat-${"mock-token-ci-variables"}`;
const TEST_PROJECT_ID = "123";
const TEST_GROUP_ID = "my-group";
const TEST_VAR_KEY = "DB_URL";
Expand Down Expand Up @@ -49,7 +49,14 @@ async function callTool(
return new Promise<any>((resolve, reject) => {
const proc = spawn("node", ["build/index.js"], {
stdio: ["pipe", "pipe", "pipe"],
env: { ...process.env, ...env },
env: {
...process.env,
...env,
SSE: "false",
STREAMABLE_HTTP: "false",
REMOTE_AUTHORIZATION: "false",
GITLAB_MCP_OAUTH: "false",
},
});

let output = "";
Expand Down Expand Up @@ -376,6 +383,10 @@ describe("CI/CD variable tools", () => {
...process.env,
GITLAB_PERSONAL_ACCESS_TOKEN: MOCK_TOKEN,
GITLAB_API_URL: `http://localhost:${mockPort}/api/v4`,
SSE: "false",
STREAMABLE_HTTP: "false",
REMOTE_AUTHORIZATION: "false",
GITLAB_MCP_OAUTH: "false",
// No GITLAB_TOOLSETS — default toolsets only
},
});
Expand Down Expand Up @@ -407,7 +418,15 @@ describe("CI/CD variable tools", () => {
return new Promise<void>((resolve, reject) => {
const proc = spawn("node", ["build/index.js"], {
stdio: ["pipe", "pipe", "pipe"],
env: { ...process.env, ...baseEnv, GITLAB_READ_ONLY_MODE: "true" },
env: {
...process.env,
...baseEnv,
GITLAB_READ_ONLY_MODE: "true",
SSE: "false",
STREAMABLE_HTTP: "false",
REMOTE_AUTHORIZATION: "false",
GITLAB_MCP_OAUTH: "false",
},
});

let output = "";
Expand Down
9 changes: 8 additions & 1 deletion test/test-create-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,14 @@ async function callCreateRepository(
return new Promise((resolve, reject) => {
const proc = spawn("node", ["build/index.js"], {
stdio: ["pipe", "pipe", "pipe"],
env: { ...process.env, ...env },
env: {
...process.env,
...env,
SSE: "false",
STREAMABLE_HTTP: "false",
REMOTE_AUTHORIZATION: "false",
GITLAB_MCP_OAUTH: "false",
},
});

let output = "";
Expand Down
Loading