Skip to content

Commit 711db20

Browse files
committed
refactor(api): add unified GitLab REST API client and fix lint errors
Introduces gitlab-api.ts utility for consistent GitLab API interactions and resolves all 101 ESLint errors without using eslint-disable comments. API CLIENT CHANGES: - Add src/utils/gitlab-api.ts with unified request handling - Centralized URL building, query params, and error handling - Support for JSON and form-urlencoded content types - Automatic GID cleanup from responses - Handle 204 No Content responses correctly OAUTH STORAGE BACKEND: - Add PostgreSQL storage backend via Prisma ORM - Add file-based storage backend for persistence - Add memory storage backend (default) - Storage factory for runtime selection - Dynamic Prisma import to avoid compile-time dependency REGISTRY REFACTORING: - Refactor labels, wiki, variables, milestones registries - Use new gitlab-api utility for cleaner request handling - Consistent pattern across all entity registries LINT FIXES: - Fix unnecessary type assertion in gitlab-api.ts - Fix Prisma type resolution with explicit interfaces - Fix logger mocks in integration and unit tests - All 101 errors resolved with proper typing Closes #17
1 parent 81e6c19 commit 711db20

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+8836
-6329
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ coverage/
2525
scripts/
2626
!scripts/cleanup-test-groups.js
2727
!scripts/test_mcp.sh
28+
!scripts/deploy.sh
2829

2930
# Keep assets
3031
!assets/
32+
33+
/generated/prisma

README.md

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -259,11 +259,13 @@ GitLab MCP Server supports OAuth 2.1 authentication for use as a **Claude Custom
259259
1. In GitLab, navigate to **User Settings > Applications** (or **Admin > Applications** for instance-wide)
260260
2. Create a new application:
261261
- **Name**: `GitLab MCP Server`
262-
- **Redirect URI**: `https://your-mcp-server.com/oauth/callback`
263-
- **Confidential**: `No` (required for device flow)
262+
- **Redirect URI**: `https://your-mcp-server.com/oauth/callback` (required for Authorization Code Flow)
263+
- **Confidential**: `No` (PKCE provides security without client secret)
264264
- **Scopes**: Select `api` and `read_user`
265265
3. Save and copy the **Application ID**
266266

267+
> **Note**: The redirect URI is used by Claude.ai Custom Connectors (Authorization Code Flow). CLI clients use Device Flow which doesn't require redirect URI.
268+
267269
#### Step 2: Configure gitlab-mcp Server
268270

269271
```bash
@@ -399,15 +401,28 @@ For GitLab instances on private networks (not internet-accessible):
399401
| Security | Token in config | No tokens in config |
400402
| Best for | Personal use, CI/CD | Teams, shared access |
401403

404+
### OAuth Flows
405+
406+
The server supports two OAuth flows automatically:
407+
408+
| Flow | Trigger | Used By | How It Works |
409+
|------|---------|---------|--------------|
410+
| **Authorization Code Flow** | `redirect_uri` present | Claude.ai Custom Connectors | Redirects to GitLab OAuth, then back to client |
411+
| **Device Flow** | No `redirect_uri` | CLI clients, Claude Desktop | Shows device code page for manual entry |
412+
413+
The flow is selected automatically based on the presence of `redirect_uri` in the authorization request.
414+
402415
### OAuth Endpoints
403416

404417
When OAuth is enabled, the following endpoints are available:
405418

406419
| Endpoint | Method | Description |
407420
|----------|--------|-------------|
408421
| `/.well-known/oauth-authorization-server` | GET | OAuth metadata discovery |
409-
| `/authorize` | GET | Start authorization (device flow) |
410-
| `/oauth/poll` | GET | Poll for authorization completion |
422+
| `/.well-known/oauth-protected-resource` | GET | Protected resource metadata (RFC 9470) |
423+
| `/authorize` | GET | Start authorization (auto-selects flow) |
424+
| `/oauth/callback` | GET | GitLab callback (Auth Code Flow only) |
425+
| `/oauth/poll` | GET | Poll for completion (Device Flow only) |
411426
| `/token` | POST | Exchange code for tokens |
412427
| `/health` | GET | Health check endpoint |
413428

eslint.config.mjs

Lines changed: 74 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,99 +1,107 @@
1-
import js from '@eslint/js';
2-
import tseslint from '@typescript-eslint/eslint-plugin';
3-
import tsparser from '@typescript-eslint/parser';
4-
import prettierPlugin from 'eslint-plugin-prettier';
1+
import js from "@eslint/js";
2+
import tseslint from "@typescript-eslint/eslint-plugin";
3+
import tsparser from "@typescript-eslint/parser";
4+
import prettierPlugin from "eslint-plugin-prettier";
55

66
export default [
77
js.configs.recommended,
88
{
9-
files: ['**/*.ts'],
10-
ignores: ['**/__tests__/**/*.ts', '**/*.test.ts', '**/*.spec.ts'],
9+
files: ["**/*.ts"],
10+
ignores: ["**/__tests__/**/*.ts", "**/*.test.ts", "**/*.spec.ts"],
1111
languageOptions: {
1212
parser: tsparser,
1313
parserOptions: {
14-
project: './tsconfig.json',
15-
sourceType: 'module',
14+
project: "./tsconfig.json",
15+
sourceType: "module",
1616
},
1717
globals: {
18-
console: 'readonly',
19-
process: 'readonly',
20-
Buffer: 'readonly',
21-
NodeJS: 'readonly',
22-
URL: 'readonly',
23-
setTimeout: 'readonly',
24-
setInterval: 'readonly',
25-
clearInterval: 'readonly',
26-
clearTimeout: 'readonly',
27-
fetch: 'readonly',
18+
console: "readonly",
19+
process: "readonly",
20+
Buffer: "readonly",
21+
NodeJS: "readonly",
22+
URL: "readonly",
23+
setTimeout: "readonly",
24+
setInterval: "readonly",
25+
clearInterval: "readonly",
26+
clearTimeout: "readonly",
27+
fetch: "readonly",
2828
},
2929
},
3030
plugins: {
31-
'@typescript-eslint': tseslint,
31+
"@typescript-eslint": tseslint,
3232
prettier: prettierPlugin,
3333
},
3434
rules: {
35-
'prettier/prettier': 'error',
36-
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
37-
'@typescript-eslint/no-floating-promises': 'error',
38-
'@typescript-eslint/no-misused-promises': 'error',
39-
'@typescript-eslint/await-thenable': 'error',
40-
'@typescript-eslint/no-unnecessary-type-assertion': 'error',
41-
'@typescript-eslint/no-unsafe-assignment': 'error',
42-
'@typescript-eslint/no-unsafe-member-access': 'error',
43-
'@typescript-eslint/no-unsafe-call': 'error',
44-
'@typescript-eslint/no-unsafe-return': 'error',
45-
'@typescript-eslint/restrict-template-expressions': 'error',
46-
'@typescript-eslint/no-unsafe-argument': 'error',
47-
'@typescript-eslint/restrict-plus-operands': 'error',
48-
'@typescript-eslint/unbound-method': 'error',
49-
'@typescript-eslint/prefer-nullish-coalescing': 'error',
50-
'@typescript-eslint/prefer-optional-chain': 'error',
51-
'@typescript-eslint/no-non-null-assertion': 'error',
52-
'@typescript-eslint/prefer-as-const': 'error',
53-
'@typescript-eslint/no-explicit-any': 'error',
54-
'@typescript-eslint/no-extraneous-class': 'off',
35+
"prettier/prettier": "error",
36+
"no-unused-vars": "off", // Disable base rule in favor of @typescript-eslint version
37+
"@typescript-eslint/no-unused-vars": [
38+
"error",
39+
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
40+
],
41+
"@typescript-eslint/no-floating-promises": "error",
42+
"@typescript-eslint/no-misused-promises": "error",
43+
"@typescript-eslint/await-thenable": "error",
44+
"@typescript-eslint/no-unnecessary-type-assertion": "error",
45+
"@typescript-eslint/no-unsafe-assignment": "error",
46+
"@typescript-eslint/no-unsafe-member-access": "error",
47+
"@typescript-eslint/no-unsafe-call": "error",
48+
"@typescript-eslint/no-unsafe-return": "error",
49+
"@typescript-eslint/restrict-template-expressions": "error",
50+
"@typescript-eslint/no-unsafe-argument": "error",
51+
"@typescript-eslint/restrict-plus-operands": "error",
52+
"@typescript-eslint/unbound-method": "error",
53+
"@typescript-eslint/prefer-nullish-coalescing": "error",
54+
"@typescript-eslint/prefer-optional-chain": "error",
55+
"@typescript-eslint/no-non-null-assertion": "error",
56+
"@typescript-eslint/prefer-as-const": "error",
57+
"@typescript-eslint/no-explicit-any": "error",
58+
"@typescript-eslint/no-extraneous-class": "off",
5559
},
5660
},
5761
{
58-
files: ['**/__tests__/**/*.ts', '**/*.test.ts', '**/*.spec.ts'],
62+
files: ["**/__tests__/**/*.ts", "**/*.test.ts", "**/*.spec.ts"],
5963
languageOptions: {
6064
parser: tsparser,
6165
parserOptions: {
62-
project: './tsconfig.json',
63-
sourceType: 'module',
66+
project: "./tsconfig.json",
67+
sourceType: "module",
6468
},
6569
globals: {
66-
console: 'readonly',
67-
process: 'readonly',
68-
Buffer: 'readonly',
69-
NodeJS: 'readonly',
70-
URL: 'readonly',
71-
setTimeout: 'readonly',
72-
setInterval: 'readonly',
73-
clearInterval: 'readonly',
74-
clearTimeout: 'readonly',
75-
fetch: 'readonly',
70+
console: "readonly",
71+
process: "readonly",
72+
Buffer: "readonly",
73+
NodeJS: "readonly",
74+
URL: "readonly",
75+
setTimeout: "readonly",
76+
setInterval: "readonly",
77+
clearInterval: "readonly",
78+
clearTimeout: "readonly",
79+
fetch: "readonly",
7680
// Jest globals
77-
describe: 'readonly',
78-
it: 'readonly',
79-
test: 'readonly',
80-
expect: 'readonly',
81-
beforeEach: 'readonly',
82-
afterEach: 'readonly',
83-
beforeAll: 'readonly',
84-
afterAll: 'readonly',
85-
jest: 'readonly',
81+
describe: "readonly",
82+
it: "readonly",
83+
test: "readonly",
84+
expect: "readonly",
85+
beforeEach: "readonly",
86+
afterEach: "readonly",
87+
beforeAll: "readonly",
88+
afterAll: "readonly",
89+
jest: "readonly",
8690
},
8791
},
8892
plugins: {
89-
'@typescript-eslint': tseslint,
93+
"@typescript-eslint": tseslint,
9094
prettier: prettierPlugin,
9195
},
9296
rules: {
93-
'prettier/prettier': 'error',
94-
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
97+
"prettier/prettier": "error",
98+
"no-unused-vars": "off", // Disable base rule in favor of @typescript-eslint version
99+
"@typescript-eslint/no-unused-vars": [
100+
"error",
101+
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
102+
],
95103
// Relax some rules for tests
96-
'@typescript-eslint/no-explicit-any': 'off',
104+
"@typescript-eslint/no-explicit-any": "off",
97105
},
98106
},
99-
];
107+
];

jest.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ module.exports = {
4545
testPathIgnorePatterns: integrationTestsEnabled ?
4646
["<rootDir>/dist/", "<rootDir>/node_modules/"] :
4747
["<rootDir>/dist/", "<rootDir>/node_modules/", "<rootDir>/tests/integration/"],
48+
modulePathIgnorePatterns: ["<rootDir>/dist/"],
4849
globalSetup: integrationTestsEnabled ? '<rootDir>/tests/setup/globalSetup.js' : undefined,
4950
globalTeardown: integrationTestsEnabled ? '<rootDir>/tests/setup/globalTeardown.js' : undefined,
5051
moduleDirectories: ["node_modules", "src"],

package.json

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"role": "original author"
1212
}
1313
],
14-
"bin": "./dist/main.js",
14+
"bin": "./dist/src/main.js",
1515
"files": [
1616
"dist"
1717
],
@@ -31,7 +31,7 @@
3131
},
3232
"scripts": {
3333
"build": "tsc -p tsconfig.build.json",
34-
"start": "node --no-warnings -r source-map-support/register --experimental-specifier-resolution=node dist/main.js",
34+
"start": "node --no-warnings -r source-map-support/register --experimental-specifier-resolution=node dist/src/main.js",
3535
"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",
3636
"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",
3737
"dev": "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",
@@ -47,46 +47,46 @@
4747
"lint:fix": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
4848
"format": "prettier --write \"**/*.{js,ts,json,md}\"",
4949
"format:check": "prettier --check \"**/*.{js,ts,json,md}\"",
50-
"list-tools": "node dist/scripts/list-tools.js",
50+
"list-tools": "node dist/src/scripts/list-tools.js",
5151
"list-tools:dev": "node --env-file=.env.test -r source-map-support/register -r ts-node/register --experimental-specifier-resolution=node src/scripts/list-tools.ts"
5252
},
5353
"dependencies": {
5454
"@graphql-typed-document-node/core": "^3.2.0",
55-
"@modelcontextprotocol/sdk": "^1.23.0",
56-
"express": "^5.1.0",
55+
"@modelcontextprotocol/sdk": "^1.25.2",
56+
"express": "^5.2.1",
5757
"form-data": "^4.0.5",
5858
"graphql": "^16.12.0",
5959
"graphql-tag": "^2.12.6",
6060
"http-proxy-agent": "^7.0.2",
6161
"https-proxy-agent": "^7.0.6",
62-
"pino": "^10.1.0",
63-
"pino-pretty": "^13.1.2",
62+
"pino": "^10.2.0",
63+
"pino-pretty": "^13.1.3",
6464
"socks-proxy-agent": "^8.0.5",
65-
"transliteration": "^2.3.5",
66-
"undici": "^7.16.0",
67-
"zod": "^4.1.13"
65+
"transliteration": "^2.6.0",
66+
"undici": "^7.18.2",
67+
"zod": "^4.3.5"
6868
},
6969
"devDependencies": {
70-
"@eslint/js": "^9.39.1",
70+
"@eslint/js": "^9.39.2",
7171
"@semantic-release/changelog": "^6.0.3",
7272
"@semantic-release/exec": "^7.1.0",
7373
"@semantic-release/git": "^10.0.1",
7474
"@semantic-release/github": "^12.0.2",
75-
"@semantic-release/npm": "^13.1.2",
76-
"@types/express": "^5.0.5",
75+
"@semantic-release/npm": "^13.1.3",
76+
"@types/express": "^5.0.6",
7777
"@types/jest": "^30.0.0",
78-
"@types/node": "^24.10.1",
79-
"@typescript-eslint/eslint-plugin": "^8.48.0",
80-
"@typescript-eslint/parser": "^8.48.0",
78+
"@types/node": "^24.10.9",
79+
"@typescript-eslint/eslint-plugin": "^8.53.0",
80+
"@typescript-eslint/parser": "^8.53.0",
8181
"auto-changelog": "^2.5.0",
8282
"cross-env": "^10.1.0",
8383
"dotenv": "^17.2.3",
84-
"eslint": "^9.39.1",
85-
"eslint-plugin-prettier": "^5.5.4",
84+
"eslint": "^9.39.2",
85+
"eslint-plugin-prettier": "^5.5.5",
8686
"jest": "^30.2.0",
87-
"prettier": "^3.6.2",
87+
"prettier": "^3.8.0",
8888
"semantic-release": "^25.0.2",
89-
"ts-jest": "^29.4.5",
89+
"ts-jest": "^29.4.6",
9090
"ts-node": "^10.9.2",
9191
"typescript": "^5.9.3"
9292
}

prisma.config.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* Prisma Configuration for gitlab-mcp OAuth Storage
3+
*
4+
* Uses OAUTH_STORAGE_POSTGRESQL_URL environment variable.
5+
* If not set, Prisma client will not be initialized.
6+
*/
7+
import "dotenv/config";
8+
import { defineConfig } from "prisma/config";
9+
10+
export default defineConfig({
11+
schema: "prisma/schema.prisma",
12+
migrations: {
13+
path: "prisma/migrations",
14+
},
15+
datasource: {
16+
// Use our consistent env variable naming
17+
url: process.env["OAUTH_STORAGE_POSTGRESQL_URL"] ?? process.env["DATABASE_URL"],
18+
},
19+
});

0 commit comments

Comments
 (0)