MCP server development guidelines
This page includes information about developing and working with the GitLab MCP server.
Set up your development environment
To set up your development environment:
- Enable and configure HTTPS in the GDK.
- Install
nodeand installmcp-remoteglobally. GDK comes with Node.js but installed AI assistants cannot use the GDK version. - Connect an AI assistant to the MCP server.
Debugging and troubleshooting
Debug Cursor
Add --debug to the mcp-remote command for more detailed logging. View MCP server logs by opening the Output and selecting
MCP:SERVERNAME. For the example below, it would be MCP:user-GitLab-GDK
{
"mcpServers": {
"GitLab-GDK": {
"command": "npx",
"args": [
"mcp-remote",
"https://gdk.test:3443/api/v4/mcp",
"--debug"
],
"env": {
"NODE_TLS_REJECT_UNAUTHORIZED": "0"
}
}
}
}Debug Claude Desktop
Check Node.js version
Claude Desktop uses an unsupported version of Node.js. Create a custom wrapper script that uses a specific version:
#!/bin/bash
# Force use of your Node.js version
NODE_BIN="/PATH_TO_NODE_INSTALL/node/22.17.0/bin/node"
MCP_REMOTE_BIN="/PATH_TO_NODE_INSTALL/node/22.17.0/bin/mcp-remote"
# Run mcp-remote with your Node.js
exec "$NODE_BIN" "$MCP_REMOTE_BIN" "$@"Use the wrapper script in the Claude Desktop configuration.
{
"mcpServers": {
"GitLab-GDK": {
"command": "/PATH_TO_REMOTE_WRAPPER_SCRIPT/mcp-remote-wrapper",
"args": [
"https://gdk.test:3443/api/v4/mcp",
"--debug"
],
"env": {
"NODE_TLS_REJECT_UNAUTHORIZED": "0"
}
}
}
}Debug mcp-remote
Test the authentication from mcp-remote to GDK outside an AI assistant:
NODE_TLS_REJECT_UNAUTHORIZED=0 npx mcp-remote https://gdk.test:3443/api/v4/mcp --debugIf you switch branches, you may experience authentication issues which can include UNABLE_TO_VERIFY_LEAF_SIGNATURE
errors in the logs. The untrusted certificate error in chain is specific to GDK instances that use TLS. The error is
caused by the https client used in Node.js and the mcp-remote library via npx. The https client doesn’t trust
certificates signed outside a bundled certificate authorities list.
If encountering authentication issues clearing your ~/.mcp-auth directory, as a last resort, resets stored
credentials for mcp-remote. When the AI Assistant reconnects to the MCP server, a browser window opens to prompt
for authorization.
rm -rf ~/.mcp-authDebug with MCP Inspector
MCP Inspector is an interactive developer tool for testing and debugging MCP servers.
The following command opens an intuitive Web UI for connecting to a server and listing and executing MCP tools:
npx -y @modelcontextprotocol/inspector npxDevelopment workflow
Helpful links
Adding a new tool
The Tool Proposal process
Our current development guidelines remain in early development. As we continue establishing tool development standards - especially for custom and
aggregated tools - we’ve created an interim mcp-tool-review-board committee to evaluate proposed tools before implementation and guide teams planning new MCP tools.
To add a new tool, please create a MCP Tool Proposal issue and follow the template instructions.
Tool implementation location depends on GitLab resource interaction:
- Tools that interact with GitLab resources should eventually live in MCP Server, but can be implemented in the Agent Platform for short-term or urgent needs.
- Tools that don’t interact with GitLab resources should be implemented in the Agent Platform.
We are working to integrate MCP server functionality into the Agent Platform. You can track progress via this issue.
We strongly encourage all engineers to follow the tool proposal process and provide clear explanations of their use cases.
Implement a tool from an API route
This merge request defines a process for creating an MCP tool from an API route.
Adding the following route_setting to an API route definition:
route_setting :mcp, tool_name: :get_merge_request, params: [:id, :merge_request_iid]- Adds a
get_merge_requesttool to the list of tools and enables its execution - The tool and parameters description is taken from the OpenAPI route definition
- The accepted parameters are filtered by the
paramsargument. For example, onlyidandmerge_request_iidare advertised and accepted - When the tool is called, the route code is executed directly with the passed parameters
This merge request provides more examples.
Implement an aggregated API tool
Aggregated API tools combine multiple related API tools into a single unified interface, reducing tool count and improving the user experience. The search tool demonstrates this pattern by consolidating global, group, and project search into one tool.
When to use aggregated tools:
Use aggregated tools when you have multiple API endpoints that serve similar purposes but operate at different scopes (global, group, project). This reduces cognitive load on the LLM by presenting one tool instead of three.
Implementation steps:
- Create an aggregated service class that inherits from
Mcp::Tools::AggregatedService:
module Mcp
module Tools
class ExampleAggregatedService < AggregatedService
include Gitlab::Utils::StrongMemoize
extend ::Gitlab::Utils::Override
register_version '0.1.0', {
description: 'My example aggregated tool',
input_schema: {
type: 'object',
properties: {},
required: []
}
}
override :tool_name
def self.tool_name
'new_tool'
end
override :select_tool
def select_tool(args)
tool_name = if args[:group_id]
:example_tool_for_group
elsif args[:project_id]
:example_tool_for_project
end
tools.find { |tool| tool.name.to_sym == tool_name }
end
override :transform_arguments
def transform_arguments(args)
if args[:group_id]
args.merge(id: args[:group_id])
elsif args[:project_id]
args.merge(id: args[:project_id])
else
args
end
end
end
end
end- Register the underlying API tools with the aggregator in their route definitions:
route_setting :mcp, tool_name: :example_tool_for_group, params: [:id], aggregators: [::Mcp::Tools::ExampleAggregatedService]
route_setting :mcp, tool_name: :example_tool_for_project, params: [:id], aggregators: [::Mcp::Tools::ExampleAggregatedService]- The
Mcp::Tools::Managerautomatically discovers aggregated tools by scanning routes withaggregatorsspecified and instantiates the aggregator class with the collected tools.
Implement a custom tool
For tools with distinct functionality that should remain separate from API exposure, you can define a standalone class (see this example for reference).
Risks of tool proliferation in AI agent architecture
Key problems with adding too many tools
- Performance degradation
- Context Bloat: Every tool definition (name, description, JSON schema) is added to the model’s prompt context, increasing input token count
- Increased Latency: Larger prompts lead to slower response times
- Higher Costs: More tokens = higher API costs per request
- Reduced Accuracy: Research shows both more context and more tools degrade agent performance
- Trajectory Degradation: Agents requiring longer reasoning paths degrade more quickly with tool proliferation
- Tool selection confusion
- Decision Complexity: More tools make it harder for the model to select the correct one
- Parameter Design Issues: Using internal IDs (like GitLab global IDs) instead of public-facing formats (project paths, IIDs) that LLMs are trained on reduces tool usage effectiveness
- Ambiguity Between Similar Tools: The model struggles to differentiate between tools with similar purposes (for example, local vs. remote file operations)
- User experience impact
- Tool Chaining Noise: More tools lead to more tool calls in a single request, creating noise and distraction
- Slower Responses: Increased processing time affects user satisfaction
- Incorrect Tool Selection: Wrong tool choices lead to incorrect or irrelevant responses
Questions Developers Should Answer Before Adding Tools
- Necessity: Is this truly a new capability, or could it be handled by an existing tool with parameter adjustments?
- Consolidation potential: Can this functionality be merged with an existing tool using an enum or parameter?
- Parameter design: Are you using publicly recognizable identifiers that align with the model’s training data?
- Evaluation strategy: How do you measure whether this tool improves or degrades overall agent performance?
- Context management: How does this tool’s definition impact the available context window for actual reasoning?
- Tool routing: Should this tool be part of a specialized sub-agent rather than the main agent’s toolset?
- Permission model: How does this tool interact with your permission/pricing model? Does it create complexity?
- Semantic distinctness: Is this tool clearly distinguishable from others in your toolset?
More tools aren’t always better. The research shows that both context size and tool count have diminishing returns and eventually lead to performance degradation. Consider tool consolidation, specialized sub-agents, or dynamic tool routing instead of continuously expanding your toolset.
Modifying an existing tool
MCP tools use semantic versioning to avoid breaking changes for consumers. When modifying a tool, use the versioning system introduced in this merge request.
Why versioning matters:
LLMs and AI agents cache tool schemas and build workflows around specific tool behaviors. Changes to tool parameters, descriptions, or output formats can break existing integrations. Versioning allows safe evolution while maintaining backward compatibility.
Version registration pattern:
For aggregated API, custom, and graphQL tools, register versions using register_version:
module Mcp
module Tools
class GetServerVersionService < CustomService
register_version '0.1.0', {
description: 'Get the current version of MCP server.',
input_schema: {
type: 'object',
properties: {},
required: []
}
}
def perform_0_1_0(_arguments = {})
data = { version: Gitlab::VERSION, revision: Gitlab.revision }
formatted_content = [{ type: 'text', text: data[:version] }]
::Mcp::Tools::Response.success(formatted_content, data)
end
override :perform_default
def perform_default(arguments = {})
perform_0_1_0(arguments)
end
end
end
endAdding a new version:
When you need to modify a tool’s behavior:
- Register the new version with updated metadata:
register_version '0.2.0', {
description: 'Get version with additional metadata.',
input_schema: {
type: 'object',
properties: {
include_metadata: {
type: 'boolean',
description: 'Include additional metadata'
}
},
required: []
}
}- Implement the version-specific method:
def perform_0_2_0(arguments = {})
data = {
version: Gitlab::VERSION,
revision: Gitlab.revision
}
if arguments[:include_metadata]
data[:metadata] = { build_date: Time.current }
end
formatted_content = [{ type: 'text', text: data[:version] }]
::Mcp::Tools::Response.success(formatted_content, data)
end- Update
perform_defaultto use the latest version:
override :perform_default
def perform_default(arguments = {})
perform_0_2_0(arguments)
endFor API tools:
API tools automatically default to version 0.1.0. The version can be specified in the route
setting if needed:
route_setting :mcp, tool_name: :get_merge_request,
params: [:id, :merge_request_iid],
version: '1.0.0'Note: API tools from routes use a single version per tool. For tools requiring multiple versions, consider implementing as a custom tool instead.
Version support policy:
The framework automatically uses the latest version when no version is specified. Consumers can request specific versions during tool calls. Follow multi-version compatibility guidelines when deprecating versions.
Renaming a tool
Renaming a tool requires using tool aliases to maintain backward compatibility. Connected clients cache tool names and do not automatically refresh when tools are renamed. The alias system introduced in this merge request allows graceful renames without breaking existing integrations.
Why aliases are necessary:
MCP clients cache the tool list from tools/list and don’t automatically re-fetch when tools
change. Renaming a tool causes clients to call a non-existent tool name, resulting in errors
or indefinite hangs. The MCP specification supports notifications/tools/list_changed to notify
clients of changes, but GitLab MCP server doesn’t implement this (tracked
in this issue).
Implementation steps:
- Override
tool_aliasesin your tool class to include the old name:
module Mcp
module Tools
class RenamedService < AggregatedService
override :tool_name
def self.tool_name
'new_name'
end
override :tool_aliases
def self.tool_aliases
['old_name']
end
end
end
endUpdate all references to use the new tool name:
- Route settings with
tool_name: - Test files
- Documentation
- Any hardcoded tool name references
- Route settings with
The
Mcp::Tools::Managerautomatically resolves aliases duringget_toolcalls, so clients using the old name continue to work.
Important notes:
list_toolsonly returns the canonical tool name, not aliases- Aliases work for all tool types: custom, GraphQL, API, and aggregated tools
- The alias resolution happens in
Manager#resolve_aliaswhich checks all tool registries - Plan to remove aliases in a future release after sufficient time for clients to update
Deprecation timeline:
Release M: Add alias and rename tool Release M+1: Remove alias (after clients have had time to refresh their tool lists)
This approach ensures zero downtime for connected clients during tool renames.