Skip to content

Commit 171b625

Browse files
authored
feat(profiles): add configuration profiles infrastructure (#60)
* feat(profiles): add configuration profiles infrastructure (#54) Add support for named configuration profiles and presets: - Profile: full config with host/auth (user-defined in ~/.config/gitlab-mcp/profiles.yaml) - Preset: settings only, NO host/auth (built-in safe for testing) Features: - ProfileLoader class with caching and validation - applyProfile() to map profile settings to env vars - --profile CLI argument and GITLAB_PROFILE env var support - Built-in presets: readonly, developer, admin - Zod schemas for type-safe validation - 67 unit tests covering loader, applicator, types Security: Built-in presets NEVER contain host/auth to prevent accidental requests to wrong GitLab instances during testing. * chore: update Yarn to 4.12.0 * ci: update Yarn to 4.12.0 in workflows * fix(ci): remove yarnPath from .yarnrc.yml (corepack handles it) * feat(profiles): add applyPreset() and improve validation - Add applyPreset() function for runtime built-in preset application - Add .strict() to PresetSchema to reject unknown fields (security) - Validate --profile CLI argument is not another flag - Validate OAuth client_secret_env environment variable - Validate cookie auth file path exists * feat(profiles): add build asset copy and improve test coverage - Add build:copy-assets script to copy YAML presets to dist - Update getBuiltinDir() to use __dirname for npm package compatibility - Add Node.js CommonJS globals to ESLint config - Add 45+ tests for applyPreset, loadAndApplyProfile, loadAndApplyPreset - Add validation tests for OAuth, cookie auth, and TLS paths - Add profile handling tests for main.ts CLI integration Closes #54 * fix(docker): update yarn version to 4.12.0 in Dockerfile * fix(profiles): cross-platform build script and duplicate --profile warning - Replace Unix shell commands with Node.js one-liner for cross-platform compatibility - Add warning when multiple --profile flags are provided * fix(ci): add explicit yarn 4.12.0 install to pr-check workflow * fix(ci): remove pull_request_target from pr-check (uses base branch workflow) * fix(profiles): add missing env var mappings Add environment variable mappings for: - allowed_groups -> GITLAB_ALLOWED_GROUP_IDS - allowed_tools -> GITLAB_ALLOWED_TOOLS (in applyProfile) - default_namespace -> GITLAB_DEFAULT_NAMESPACE These fields were defined in the Profile type but not applied to environment variables when the profile was loaded. * fix(profiles): improve validation and address review feedback - Improve denied_actions validation to check both tool and action parts are non-empty (fixes ':action', 'tool:', ':' edge cases) - Fix getProfileNameFromEnv JSDoc comment accuracy - Move multiple --profile warning outside loop with count - Add tests for denied_actions edge cases * test(profiles): improve coverage for applicator and loader Add tests for: - allowed_groups, allowed_tools, default_namespace env var mapping - validation warnings logging path - getProfileNameFromEnv standalone function Coverage improved: - applicator.ts: 94.44% -> 99.3% - loader.ts: 95.56% -> 96.2% - Overall profiles: 91.07% -> 93.53% * docs(ci): add comment explaining pull_request trigger choice * test(profiles): improve coverage for loader Add tests for: - Alphabetical sorting within same profile category - Config cache hit when loading multiple profiles Coverage improved: - loader.ts: 96.2% -> 97.46% - Overall profiles: 93.53% -> 94.15% * fix(profiles): add warning for denied_actions with extra whitespace Warn users when denied_actions entries contain extra whitespace around the colon (e.g., "tool : action"). The validation still passes but logs a warning indicating the normalized form. * refactor(profiles): extract validateDeniedActions helper method Reduces code duplication between validateProfile and validatePreset by extracting shared denied_actions validation logic into a private helper method.
1 parent afc1ecc commit 171b625

File tree

22 files changed

+3515
-78
lines changed

22 files changed

+3515
-78
lines changed

.github/workflows/ci-cd.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ jobs:
4747
run: corepack enable
4848

4949
- name: Install Yarn
50-
run: corepack prepare yarn@4.9.4 --activate
50+
run: corepack prepare yarn@4.12.0 --activate
5151

5252
- name: Install dependencies
5353
run: yarn install --frozen-lockfile
@@ -155,7 +155,7 @@ jobs:
155155
run: corepack enable
156156

157157
- name: Install Yarn
158-
run: corepack prepare yarn@4.9.4 --activate
158+
run: corepack prepare yarn@4.12.0 --activate
159159

160160
- name: Install dependencies
161161
run: yarn install --frozen-lockfile

.github/workflows/npm-publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ jobs:
2121
run: corepack enable
2222

2323
- name: Install Yarn
24-
run: corepack prepare yarn@4.9.4 --activate
24+
run: corepack prepare yarn@4.12.0 --activate
2525

2626
- name: Install dependencies
2727
run: yarn install --frozen-lockfile

.github/workflows/pr-check.yml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
name: Pull Request Checks
22

3+
# Uses pull_request (not pull_request_target) to run workflow from PR branch
4+
# and ensure PR changes to workflow take effect immediately
35
on:
46
pull_request:
57
branches: [main]
6-
pull_request_target:
7-
branches: [main]
88

99
permissions:
1010
contents: read
@@ -27,6 +27,9 @@ jobs:
2727
- name: Enable Corepack
2828
run: corepack enable
2929

30+
- name: Install Yarn
31+
run: corepack prepare [email protected] --activate
32+
3033
- name: Install dependencies
3134
run: yarn install --frozen-lockfile
3235

.github/workflows/pr-test.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ jobs:
2626
run: corepack enable
2727

2828
- name: Install Yarn
29-
run: corepack prepare yarn@4.9.4 --activate
29+
run: corepack prepare yarn@4.12.0 --activate
3030

3131
- name: Install dependencies
3232
run: yarn install --frozen-lockfile
@@ -74,7 +74,7 @@ jobs:
7474
run: corepack enable
7575

7676
- name: Install Yarn
77-
run: corepack prepare yarn@4.9.4 --activate
77+
run: corepack prepare yarn@4.12.0 --activate
7878

7979
- name: Install dependencies
8080
run: yarn install --frozen-lockfile
@@ -119,7 +119,7 @@ jobs:
119119
run: corepack enable
120120

121121
- name: Install Yarn
122-
run: corepack prepare yarn@4.9.4 --activate
122+
run: corepack prepare yarn@4.12.0 --activate
123123

124124
- name: Install dependencies
125125
run: yarn install --frozen-lockfile
@@ -161,7 +161,7 @@ jobs:
161161
run: corepack enable
162162

163163
- name: Install Yarn
164-
run: corepack prepare yarn@4.9.4 --activate
164+
run: corepack prepare yarn@4.12.0 --activate
165165

166166
- name: Install dependencies
167167
run: yarn install --frozen-lockfile

Dockerfile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
FROM node:22-alpine AS dependencies
88

99
# Enable Corepack and prepare Yarn
10-
RUN corepack enable && corepack prepare yarn@4.9.4 --activate
10+
RUN corepack enable && corepack prepare yarn@4.12.0 --activate
1111

1212
# Set working directory
1313
WORKDIR /app
@@ -26,7 +26,7 @@ RUN --mount=type=cache,target=/root/.yarn/berry/cache \
2626
FROM node:22-alpine AS builder
2727

2828
# Enable Corepack and prepare Yarn (same version)
29-
RUN corepack enable && corepack prepare yarn@4.9.4 --activate
29+
RUN corepack enable && corepack prepare yarn@4.12.0 --activate
3030

3131
# Set working directory
3232
WORKDIR /app
@@ -54,7 +54,7 @@ RUN yarn build
5454
FROM node:22-alpine AS production-deps
5555

5656
# Enable Corepack and prepare Yarn (same version)
57-
RUN corepack enable && corepack prepare yarn@4.9.4 --activate
57+
RUN corepack enable && corepack prepare yarn@4.12.0 --activate
5858

5959
# Set working directory
6060
WORKDIR /app

eslint.config.mjs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ export default [
2525
clearInterval: "readonly",
2626
clearTimeout: "readonly",
2727
fetch: "readonly",
28+
// Node.js CommonJS globals
29+
__dirname: "readonly",
30+
__filename: "readonly",
31+
module: "readonly",
32+
require: "readonly",
2833
},
2934
},
3035
plugins: {

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"description": "MCP server for using the GitLab API",
55
"license": "Apache-2.0",
66
"author": "Dmitry Prudnikov <[email protected]>",
7-
"packageManager": "yarn@4.9.4",
7+
"packageManager": "yarn@4.12.0",
88
"contributors": [
99
{
1010
"name": "zereight",
@@ -31,7 +31,8 @@
3131
},
3232
"scripts": {
3333
"postinstall": "test -f prisma/schema.prisma && prisma generate || true",
34-
"build": "prisma generate && tsc -p tsconfig.build.json",
34+
"build": "prisma generate && tsc -p tsconfig.build.json && yarn build:copy-assets",
35+
"build:copy-assets": "node -e \"const fs=require('fs'),p=require('path');const src='src/profiles/builtin',dst='dist/src/profiles/builtin';fs.mkdirSync(dst,{recursive:true});fs.readdirSync(src).filter(f=>f.endsWith('.yaml')).forEach(f=>fs.copyFileSync(p.join(src,f),p.join(dst,f)))\"",
3536
"start": "node --no-warnings -r source-map-support/register --experimental-specifier-resolution=node dist/src/main.js",
3637
"dev:stdio": "node --env-file=.env.test -r source-map-support/register -r ts-node/register --experimental-specifier-resolution=node --experimental-print-required-tla --watch src/main.ts stdio",
3738
"dev:sse": "node --env-file=.env.test -r source-map-support/register -r ts-node/register --experimental-specifier-resolution=node --experimental-print-required-tla --watch src/main.ts sse",
@@ -67,6 +68,7 @@
6768
"socks-proxy-agent": "^8.0.5",
6869
"transliteration": "^2.6.0",
6970
"undici": "^7.18.2",
71+
"yaml": "^2.8.2",
7072
"zod": "^4.3.5"
7173
},
7274
"devDependencies": {

src/main.ts

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,69 @@
22

33
import { startServer } from "./server";
44
import { logger } from "./logger";
5+
import { tryApplyProfileFromEnv } from "./profiles";
56

6-
// Start the server
7-
startServer().catch((error: unknown) => {
7+
/**
8+
* Parse CLI arguments for --profile flag
9+
*/
10+
function getProfileFromArgs(): string | undefined {
11+
const args = process.argv.slice(2);
12+
let profileName: string | undefined;
13+
let profileCount = 0;
14+
15+
for (let i = 0; i < args.length; i++) {
16+
if (args[i] === "--profile") {
17+
const value = args[i + 1];
18+
// Validate that value exists and is not another flag
19+
if (!value || value.startsWith("--")) {
20+
logger.error("--profile requires a profile name (e.g., --profile work)");
21+
process.exit(1);
22+
}
23+
profileCount++;
24+
if (profileCount === 1) {
25+
profileName = value;
26+
}
27+
}
28+
}
29+
30+
if (profileCount > 1) {
31+
logger.warn({ count: profileCount }, "Multiple --profile flags detected, using first value");
32+
}
33+
34+
return profileName;
35+
}
36+
37+
/**
38+
* Main entry point
39+
*/
40+
async function main(): Promise<void> {
41+
// Apply profile if specified (CLI arg > env var > default)
42+
const profileName = getProfileFromArgs();
43+
try {
44+
const result = await tryApplyProfileFromEnv(profileName);
45+
if (result) {
46+
// Handle both profile and preset results
47+
if ("profileName" in result) {
48+
logger.info(
49+
{ profile: result.profileName, host: result.host },
50+
"Using configuration profile"
51+
);
52+
} else {
53+
logger.info({ preset: result.presetName }, "Using configuration preset");
54+
}
55+
}
56+
} catch (error) {
57+
// Profile errors are fatal - don't start with misconfigured profile
58+
const message = error instanceof Error ? error.message : String(error);
59+
logger.error({ error: message }, "Failed to load profile");
60+
process.exit(1);
61+
}
62+
63+
// Start the server
64+
await startServer();
65+
}
66+
67+
main().catch((error: unknown) => {
868
logger.error(`Failed to start GitLab MCP Server: ${String(error)}`);
969
process.exit(1);
1070
});

0 commit comments

Comments
 (0)