Architecture
July 10, 2026 · View on GitHub
This document describes the internal design of gitlab-mcp, its module relationships, and key design decisions.
Overview
gitlab-mcp is structured as a layered MCP server with clear separation between transport, tool registration, API client, and cross-cutting concerns (policy, auth, output formatting).
┌──────────────────────────────────────────────────────┐
│ Transport Layer │
│ index.ts (stdio) │ http.ts (HTTP/SSE) │
└─────────┬──────────┴──────────┬──────────────────────┘
│ │
│ ┌────────────────▼────────────────────┐
│ │ Session Management (HTTP only) │
│ │ - Serial request queuing │
│ │ - Rate limiting per session │
│ │ - TTL-based garbage collection │
│ │ - AsyncLocalStorage auth context │
│ └────────────────┬────────────────────┘
│ │
┌─────────▼─────────────────────▼──────────────────────┐
│ MCP Server Factory │
│ build-server.ts │
│ ┌──────────────────────────────────────────────┐ │
│ │ registerHealthTool() │ │
│ │ registerGitLabTools() ──▶ Policy filtering │ │
│ └──────────────────────────────────────────────┘ │
└───────────────────────┬──────────────────────────────┘
│
┌───────────────────────▼──────────────────────────────┐
│ AppContext │
│ ┌──────────┐ ┌───────────┐ ┌──────────────────┐ │
│ │ env │ │ logger │ │ gitlab (Client) │ │
│ │ (AppEnv) │ │ (Pino) │ │ │ │
│ └──────────┘ └───────────┘ └──────────────────┘ │
│ ┌──────────────────┐ ┌─────────────────────────┐ │
│ │ policy (Engine) │ │ formatter (Output) │ │
│ └──────────────────┘ └─────────────────────────┘ │
└──────────────────────────────────────────────────────┘
Module Dependency Graph
config/env.ts
▲
│ (imported by all entry points)
│
├── index.ts (stdio)
│ └── lib/gitlab-client.ts
│ └── lib/policy.ts
│ └── lib/output.ts
│ └── lib/network.ts
│ └── lib/request-runtime.ts
│ └── lib/oauth.ts
│ └── server/build-server.ts
│ └── tools/gitlab.ts
│ │ └── tools/mr-code-context.ts
│ └── tools/health.ts
│
└── http.ts (HTTP)
└── (same dependencies as index.ts)
└── lib/auth-context.ts (AsyncLocalStorage)
Key Modules
config/env.ts — Configuration
- Parses and validates all environment variables using Zod schemas
- Enforces cross-field constraints (e.g. OAuth requires client ID, dynamic API URL requires remote auth)
- Normalizes API URLs to
/api/v4suffix - Exports a typed
envsingleton andAppEnvtype - Fails fast at startup with descriptive error messages
server/build-server.ts — Server Factory
- Creates an
McpServerinstance with configured name and version - Registers the health check tool
- Registers all GitLab tools after policy filtering
- Pure factory function — no side effects
tools/gitlab.ts — Tool Definitions
- Defines the complete registry as a
GitLabToolDefinition[]array; the generated index is the authoritative inventory - Each definition specifies:
name,title,description,capabilities, optionalrequiresFeature, project/group/global/raw-GraphQL scope metadata,inputSchema(Zod), andhandler - Tools are filtered by project-scope metadata and the policy engine at registration time
- Tool execution wraps results through the output formatter
- Error handling converts
GitLabApiErrorto structured MCP error responses
Tool execution flow:
Raw args ──▶ sanitizeToolArguments ──▶ scope guard ──▶ handler ──▶ success redaction ──▶ formatter ──▶ MCP response
│ │
│ └── assertAuthReady() (check token exists)
│ └── resolveProjectId() (defense in depth)
└── validate source/target/parent project arguments
When GITLAB_ALLOWED_PROJECT_IDS is configured, registration keeps project-bound tools and explicitly safe/filterable global tools. Group-wide, unprovable global, and raw GraphQL tools are removed. Execution repeats project/group argument checks so a stale or direct tool call cannot bypass registration filtering. Handlers additionally filter list responses and verify resources such as todos before mutation.
tools/mr-code-context.ts — MR Code Context
A specialized tool for AI-assisted code review:
- Fetches MR diff files
- Filters by glob patterns, extensions, or languages
- Sorts by changed lines, path, or file size
- Retrieves file content within a character budget
- Supports three content modes:
- patch — Raw unified diff
- surrounding — Changed lines with N lines of context from the full file
- fullfile — Complete file content
- Supports
list_onlymode for two-stage retrieval (list first, then fetch selectively)
lib/gitlab-client.ts — GitLab API Client
- Wraps the GitLab REST API v4 with typed methods
- Supports multi-instance URL rotation (round-robin)
- Pre-request hook system (
beforeRequest) for token/header injection - Per-session auth via
AsyncLocalStorage(checked before each request) - Configurable timeout with
AbortSignal - Error wrapping via
GitLabApiErrorwith status code and details
Request lifecycle:
Method call ──▶ Build URL ──▶ Set headers ──▶ beforeRequest hook
│ │
│ ┌───────────────────┘
│ ▼
│ Apply session auth (if available)
│ Apply token header
│ Apply compatibility headers
│ │
└──────────────────────────▼
fetch() with timeout
│
Parse response / throw GitLabApiError
lib/policy.ts — Tool Policy Engine
Controls which tools are available:
- Read-only mode — Blocks tools that require
write,delete, oradmincapabilities - Capability denylist — Blocks tools tagged with disabled capabilities such as
deleteorgraphql - Feature toggles — Blocks tools requiring disabled features (wiki, milestone, pipeline, release)
- Allowlist — If set, only listed tools are available. Tool names are normalized (accepts
get_projectorgitlab_get_project) - Deny regex — Blocks tools matching a regex pattern
Policy is applied in two places:
- Registration time —
filterTools()removes tools from the MCP server entirely - Execution time —
assertCanExecute()double-checks (defense in depth)
lib/auth-context.ts — Session Auth Context
Uses Node.js AsyncLocalStorage to provide per-request authentication context:
interface SessionAuth {
sessionId?: string;
token?: string;
apiUrl?: string;
header?: "authorization" | "private-token" | "job-token";
updatedAt: number;
}
- In HTTP mode, each request runs within
runWithSessionAuth()which sets the context - The GitLab client reads
getSessionAuth()to get per-request credentials - In stdio mode, session auth is not used (static PAT is the primary method)
lib/request-runtime.ts — Request Preprocessing
Orchestrates authentication and request modifications:
- Cookie management — Loads Netscape cookie files, auto-reloads on changes, creates
fetch-cookiewrapper - Session warmup — Sends a warmup request to establish cookie sessions
- Token resolution — When no request/session/PAT token is present, tries OAuth, then token script, then token file
- Compatibility headers — Applies User-Agent, Accept-Language for Cloudflare bypass
- Token caching — Caches resolved tokens with configurable TTL
lib/oauth.ts — OAuth PKCE Manager
Implements the full OAuth 2.0 PKCE flow:
- Check stored token → use if not expired
- Try refresh token → persist new token
- Fall back to interactive flow:
- Generate PKCE challenge
- Build authorization URL
- Start local HTTP callback server
- Open browser (optional)
- Wait for callback (3 minute timeout)
- Exchange code for token
- Persist token (chmod 600)
lib/network.ts — Network Runtime
Configures global fetch behavior using undici:
- Sets up proxy agent (
ProxyAgent) ifHTTP_PROXY/HTTPS_PROXYis set - Loads custom CA certificates from
GITLAB_CA_CERT_PATH - Controls TLS verification via
rejectUnauthorized - Applied globally via
setGlobalDispatcher()
lib/output.ts — Response Formatting
- Serializes tool output to JSON (pretty), compact JSON, or YAML
- Enforces
GITLAB_MAX_RESPONSE_BYTESlimit - Truncates oversized responses with a
[truncated N bytes]marker - Returns metadata:
truncatedflag andbytescount
lib/sanitize.ts — Null Stripping
stripNullsDeep() recursively removes null values from objects and arrays before passing them to the GitLab API. This prevents sending null in JSON payloads where undefined (omission) is the correct behavior.
HTTP Server Session Management
The HTTP server (http.ts) implements a sophisticated session management system:
Streamable HTTP Sessions
Client POST /mcp (no session-id)
└── Create new session
├── Create McpServer instance
├── Create StreamableHTTPServerTransport
├── Add to pending sessions
├── Connect server to transport
├── On session init → move to active sessions
└── Return Mcp-Session-Id header
Client POST /mcp (with Mcp-Session-Id)
└── Look up existing session
├── Refresh auth from request headers
├── Check rate limit
├── Enqueue request (serial per session)
└── Process within session auth context
Session Lifecycle
- Creation — New session created on first POST without session ID
- Active — Session receives requests, each queued serially
- Idle timeout — Garbage collected after
SESSION_TIMEOUT_SECONDSof inactivity - Rate limited — Returns 429 after
MAX_REQUESTS_PER_MINUTEper session - Capacity — Returns 503 when
MAX_SESSIONSis reached - Shutdown — All sessions closed gracefully on SIGINT/SIGTERM
SSE Sessions (Legacy)
When SSE=true:
- Clients connect via
GET /sseto establish an SSE stream - Messages are sent via
POST /messages?sessionId=... - Sessions are cleaned up on client disconnect or idle timeout
Design Patterns
Dependency Injection via AppContext
All shared services are bundled into an AppContext interface:
interface AppContext {
env: AppEnv;
logger: Logger;
gitlab: GitLabClient;
policy: ToolPolicyEngine;
formatter: OutputFormatter;
}
This is created once at startup and passed to tool registration functions. Tools access all services through this context.
Null Preprocessing in Zod Schemas
Many MCP clients send null for optional parameters. The tool schemas use z.preprocess() to convert null to undefined:
const optionalString = z.preprocess(
(value) => (value === null ? undefined : value),
z.string().optional()
);
Backward Compatibility Aliases
Several tools have backward-compatible aliases to support existing integrations:
gitlab_mr_discussions→ alias ofgitlab_list_merge_request_discussionsgitlab_get_merge_request_notes→ alias ofgitlab_list_merge_request_notesgitlab_edit_milestone→ alias ofgitlab_update_milestonegitlab_execute_graphql→ backward-compatible executor honoring permission-mode policy
Structured Content in Responses
Tool responses include both text content (for display) and structured content (for programmatic access):
return {
content: [{ type: "text", text: formatted.text }],
structuredContent: {
result: toStructuredContent(result),
meta: { truncated: formatted.truncated, bytes: formatted.bytes }
}
};