Skip to content

Commit 95ad321

Browse files
committed
feat: Decode project_id for GitLab API calls
- Decode project_id using decodeURIComponent() in relevant helper functions. - This resolves API call issues related to project ID encoding differences between models. - Updated CHANGELOG for 1.0.36 and added thanks to Aubermean.
1 parent a33749d commit 95ad321

File tree

3 files changed

+72
-6
lines changed

3 files changed

+72
-6
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
## [Released] - 2025-05-13
2+
3+
### Fixed
4+
5+
- **GitLab MCP Server:** Modified GitLab API helper functions to decode the `project_id` using `decodeURIComponent()` before processing. This resolves API call failures caused by differences in project ID encoding between Gemini and other AI models. API requests are now handled consistently regardless of the model.

index.ts

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,8 @@ const allTools = [
395395
},
396396
{
397397
name: "get_repository_tree",
398-
description: "Get the repository tree for a GitLab project (list files and directories)",
398+
description:
399+
"Get the repository tree for a GitLab project (list files and directories)",
399400
inputSchema: zodToJsonSchema(GetRepositoryTreeSchema),
400401
},
401402
];
@@ -506,6 +507,7 @@ async function forkProject(
506507
projectId: string,
507508
namespace?: string
508509
): Promise<GitLabFork> {
510+
projectId = decodeURIComponent(projectId); // Decode project ID
509511
const url = new URL(
510512
`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/fork`
511513
);
@@ -541,6 +543,7 @@ async function createBranch(
541543
projectId: string,
542544
options: z.infer<typeof CreateBranchOptionsSchema>
543545
): Promise<GitLabReference> {
546+
projectId = decodeURIComponent(projectId); // Decode project ID
544547
const url = new URL(
545548
`${GITLAB_API_URL}/projects/${encodeURIComponent(
546549
projectId
@@ -568,6 +571,7 @@ async function createBranch(
568571
* @returns {Promise<string>} The name of the default branch
569572
*/
570573
async function getDefaultBranchRef(projectId: string): Promise<string> {
574+
projectId = decodeURIComponent(projectId); // Decode project ID
571575
const url = new URL(
572576
`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}`
573577
);
@@ -595,6 +599,7 @@ async function getFileContents(
595599
filePath: string,
596600
ref?: string
597601
): Promise<GitLabContent> {
602+
projectId = decodeURIComponent(projectId); // Decode project ID
598603
const encodedPath = encodeURIComponent(filePath);
599604

600605
// ref가 없는 경우 default branch를 가져옴
@@ -646,6 +651,7 @@ async function createIssue(
646651
projectId: string,
647652
options: z.infer<typeof CreateIssueOptionsSchema>
648653
): Promise<GitLabIssue> {
654+
projectId = decodeURIComponent(projectId); // Decode project ID
649655
const url = new URL(
650656
`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues`
651657
);
@@ -685,6 +691,7 @@ async function listIssues(
685691
projectId: string,
686692
options: Omit<z.infer<typeof ListIssuesSchema>, "project_id"> = {}
687693
): Promise<GitLabIssue[]> {
694+
projectId = decodeURIComponent(projectId); // Decode project ID
688695
const url = new URL(
689696
`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues`
690697
);
@@ -722,6 +729,7 @@ async function getIssue(
722729
projectId: string,
723730
issueIid: number
724731
): Promise<GitLabIssue> {
732+
projectId = decodeURIComponent(projectId); // Decode project ID
725733
const url = new URL(
726734
`${GITLAB_API_URL}/projects/${encodeURIComponent(
727735
projectId
@@ -751,6 +759,7 @@ async function updateIssue(
751759
issueIid: number,
752760
options: Omit<z.infer<typeof UpdateIssueSchema>, "project_id" | "issue_iid">
753761
): Promise<GitLabIssue> {
762+
projectId = decodeURIComponent(projectId); // Decode project ID
754763
const url = new URL(
755764
`${GITLAB_API_URL}/projects/${encodeURIComponent(
756765
projectId
@@ -783,6 +792,7 @@ async function updateIssue(
783792
* @returns {Promise<void>}
784793
*/
785794
async function deleteIssue(projectId: string, issueIid: number): Promise<void> {
795+
projectId = decodeURIComponent(projectId); // Decode project ID
786796
const url = new URL(
787797
`${GITLAB_API_URL}/projects/${encodeURIComponent(
788798
projectId
@@ -809,6 +819,7 @@ async function listIssueLinks(
809819
projectId: string,
810820
issueIid: number
811821
): Promise<GitLabIssueWithLinkDetails[]> {
822+
projectId = decodeURIComponent(projectId); // Decode project ID
812823
const url = new URL(
813824
`${GITLAB_API_URL}/projects/${encodeURIComponent(
814825
projectId
@@ -838,6 +849,7 @@ async function getIssueLink(
838849
issueIid: number,
839850
issueLinkId: number
840851
): Promise<GitLabIssueLink> {
852+
projectId = decodeURIComponent(projectId); // Decode project ID
841853
const url = new URL(
842854
`${GITLAB_API_URL}/projects/${encodeURIComponent(
843855
projectId
@@ -871,6 +883,8 @@ async function createIssueLink(
871883
targetIssueIid: number,
872884
linkType: "relates_to" | "blocks" | "is_blocked_by" = "relates_to"
873885
): Promise<GitLabIssueLink> {
886+
projectId = decodeURIComponent(projectId); // Decode project ID
887+
targetProjectId = decodeURIComponent(targetProjectId); // Decode target project ID as well
874888
const url = new URL(
875889
`${GITLAB_API_URL}/projects/${encodeURIComponent(
876890
projectId
@@ -906,6 +920,7 @@ async function deleteIssueLink(
906920
issueIid: number,
907921
issueLinkId: number
908922
): Promise<void> {
923+
projectId = decodeURIComponent(projectId); // Decode project ID
909924
const url = new URL(
910925
`${GITLAB_API_URL}/projects/${encodeURIComponent(
911926
projectId
@@ -932,6 +947,7 @@ async function createMergeRequest(
932947
projectId: string,
933948
options: z.infer<typeof CreateMergeRequestOptionsSchema>
934949
): Promise<GitLabMergeRequest> {
950+
projectId = decodeURIComponent(projectId); // Decode project ID
935951
const url = new URL(
936952
`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/merge_requests`
937953
);
@@ -977,6 +993,7 @@ async function listMergeRequestDiscussions(
977993
projectId: string,
978994
mergeRequestIid: number
979995
): Promise<GitLabDiscussion[]> {
996+
projectId = decodeURIComponent(projectId); // Decode project ID
980997
const url = new URL(
981998
`${GITLAB_API_URL}/projects/${encodeURIComponent(
982999
projectId
@@ -1013,6 +1030,7 @@ async function updateMergeRequestNote(
10131030
body: string,
10141031
resolved?: boolean
10151032
): Promise<GitLabDiscussionNote> {
1033+
projectId = decodeURIComponent(projectId); // Decode project ID
10161034
const url = new URL(
10171035
`${GITLAB_API_URL}/projects/${encodeURIComponent(
10181036
projectId
@@ -1057,6 +1075,7 @@ async function createOrUpdateFile(
10571075
last_commit_id?: string,
10581076
commit_id?: string
10591077
): Promise<GitLabCreateUpdateFileResponse> {
1078+
projectId = decodeURIComponent(projectId); // Decode project ID
10601079
const encodedPath = encodeURIComponent(filePath);
10611080
const url = new URL(
10621081
`${GITLAB_API_URL}/projects/${encodeURIComponent(
@@ -1139,6 +1158,7 @@ async function createTree(
11391158
files: FileOperation[],
11401159
ref?: string
11411160
): Promise<GitLabTree> {
1161+
projectId = decodeURIComponent(projectId); // Decode project ID
11421162
const url = new URL(
11431163
`${GITLAB_API_URL}/projects/${encodeURIComponent(
11441164
projectId
@@ -1193,6 +1213,7 @@ async function createCommit(
11931213
branch: string,
11941214
actions: FileOperation[]
11951215
): Promise<GitLabCommit> {
1216+
projectId = decodeURIComponent(projectId); // Decode project ID
11961217
const url = new URL(
11971218
`${GITLAB_API_URL}/projects/${encodeURIComponent(
11981219
projectId
@@ -1325,6 +1346,7 @@ async function getMergeRequest(
13251346
mergeRequestIid?: number,
13261347
branchName?: string
13271348
): Promise<GitLabMergeRequest> {
1349+
projectId = decodeURIComponent(projectId); // Decode project ID
13281350
let url: URL;
13291351

13301352
if (mergeRequestIid) {
@@ -1375,6 +1397,7 @@ async function getMergeRequestDiffs(
13751397
branchName?: string,
13761398
view?: "inline" | "parallel"
13771399
): Promise<GitLabMergeRequestDiff[]> {
1400+
projectId = decodeURIComponent(projectId); // Decode project ID
13781401
if (!mergeRequestIid && !branchName) {
13791402
throw new Error("Either mergeRequestIid or branchName must be provided");
13801403
}
@@ -1426,6 +1449,7 @@ async function updateMergeRequest(
14261449
mergeRequestIid?: number,
14271450
branchName?: string
14281451
): Promise<GitLabMergeRequest> {
1452+
projectId = decodeURIComponent(projectId); // Decode project ID
14291453
if (!mergeRequestIid && !branchName) {
14301454
throw new Error("Either mergeRequestIid or branchName must be provided");
14311455
}
@@ -1472,6 +1496,7 @@ async function createNote(
14721496
noteableIid: number,
14731497
body: string
14741498
): Promise<any> {
1499+
projectId = decodeURIComponent(projectId); // Decode project ID
14751500
// ⚙️ 응답 타입은 GitLab API 문서에 따라 조정 가능
14761501
const url = new URL(
14771502
`${GITLAB_API_URL}/projects/${encodeURIComponent(
@@ -1600,6 +1625,7 @@ async function getProject(
16001625
with_custom_attributes?: boolean;
16011626
} = {}
16021627
): Promise<GitLabProject> {
1628+
projectId = decodeURIComponent(projectId); // Decode project ID
16031629
const url = new URL(
16041630
`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}`
16051631
);
@@ -1674,6 +1700,7 @@ async function listLabels(
16741700
projectId: string,
16751701
options: Omit<z.infer<typeof ListLabelsSchema>, "project_id"> = {}
16761702
): Promise<GitLabLabel[]> {
1703+
projectId = decodeURIComponent(projectId); // Decode project ID
16771704
// Construct the URL with project path
16781705
const url = new URL(
16791706
`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/labels`
@@ -1716,6 +1743,7 @@ async function getLabel(
17161743
labelId: number | string,
17171744
includeAncestorGroups?: boolean
17181745
): Promise<GitLabLabel> {
1746+
projectId = decodeURIComponent(projectId); // Decode project ID
17191747
const url = new URL(
17201748
`${GITLAB_API_URL}/projects/${encodeURIComponent(
17211749
projectId
@@ -1754,6 +1782,7 @@ async function createLabel(
17541782
projectId: string,
17551783
options: Omit<z.infer<typeof CreateLabelSchema>, "project_id">
17561784
): Promise<GitLabLabel> {
1785+
projectId = decodeURIComponent(projectId); // Decode project ID
17571786
// Make the API request
17581787
const response = await fetch(
17591788
`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/labels`,
@@ -1785,6 +1814,7 @@ async function updateLabel(
17851814
labelId: number | string,
17861815
options: Omit<z.infer<typeof UpdateLabelSchema>, "project_id" | "label_id">
17871816
): Promise<GitLabLabel> {
1817+
projectId = decodeURIComponent(projectId); // Decode project ID
17881818
// Make the API request
17891819
const response = await fetch(
17901820
`${GITLAB_API_URL}/projects/${encodeURIComponent(
@@ -1815,6 +1845,7 @@ async function deleteLabel(
18151845
projectId: string,
18161846
labelId: number | string
18171847
): Promise<void> {
1848+
projectId = decodeURIComponent(projectId); // Decode project ID
18181849
// Make the API request
18191850
const response = await fetch(
18201851
`${GITLAB_API_URL}/projects/${encodeURIComponent(
@@ -1908,6 +1939,7 @@ async function listWikiPages(
19081939
projectId: string,
19091940
options: Omit<z.infer<typeof ListWikiPagesSchema>, "project_id"> = {}
19101941
): Promise<GitLabWikiPage[]> {
1942+
projectId = decodeURIComponent(projectId); // Decode project ID
19111943
const url = new URL(
19121944
`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/wikis`
19131945
);
@@ -1929,6 +1961,7 @@ async function getWikiPage(
19291961
projectId: string,
19301962
slug: string
19311963
): Promise<GitLabWikiPage> {
1964+
projectId = decodeURIComponent(projectId); // Decode project ID
19321965
const response = await fetch(
19331966
`${GITLAB_API_URL}/projects/${encodeURIComponent(
19341967
projectId
@@ -1949,6 +1982,7 @@ async function createWikiPage(
19491982
content: string,
19501983
format?: string
19511984
): Promise<GitLabWikiPage> {
1985+
projectId = decodeURIComponent(projectId); // Decode project ID
19521986
const body: Record<string, any> = { title, content };
19531987
if (format) body.format = format;
19541988
const response = await fetch(
@@ -1974,6 +2008,7 @@ async function updateWikiPage(
19742008
content?: string,
19752009
format?: string
19762010
): Promise<GitLabWikiPage> {
2011+
projectId = decodeURIComponent(projectId); // Decode project ID
19772012
const body: Record<string, any> = {};
19782013
if (title) body.title = title;
19792014
if (content) body.content = content;
@@ -1997,6 +2032,7 @@ async function updateWikiPage(
19972032
* Delete a wiki page
19982033
*/
19992034
async function deleteWikiPage(projectId: string, slug: string): Promise<void> {
2035+
projectId = decodeURIComponent(projectId); // Decode project ID
20002036
const response = await fetch(
20012037
`${GITLAB_API_URL}/projects/${encodeURIComponent(
20022038
projectId
@@ -2018,16 +2054,20 @@ async function deleteWikiPage(projectId: string, slug: string): Promise<void> {
20182054
async function getRepositoryTree(
20192055
options: GetRepositoryTreeOptions
20202056
): Promise<GitLabTreeItem[]> {
2057+
options.project_id = decodeURIComponent(options.project_id); // Decode project_id within options
20212058
const queryParams = new URLSearchParams();
20222059
if (options.path) queryParams.append("path", options.path);
20232060
if (options.ref) queryParams.append("ref", options.ref);
20242061
if (options.recursive) queryParams.append("recursive", "true");
2025-
if (options.per_page) queryParams.append("per_page", options.per_page.toString());
2062+
if (options.per_page)
2063+
queryParams.append("per_page", options.per_page.toString());
20262064
if (options.page_token) queryParams.append("page_token", options.page_token);
20272065
if (options.pagination) queryParams.append("pagination", options.pagination);
20282066

20292067
const response = await fetch(
2030-
`${GITLAB_API_URL}/projects/${encodeURIComponent(options.project_id)}/repository/tree?${queryParams.toString()}`,
2068+
`${GITLAB_API_URL}/projects/${encodeURIComponent(
2069+
options.project_id
2070+
)}/repository/tree?${queryParams.toString()}`,
20312071
{
20322072
headers: {
20332073
Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
@@ -2054,12 +2094,33 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
20542094
? allTools.filter((tool) => readOnlyTools.includes(tool.name))
20552095
: allTools;
20562096
// Toggle wiki tools by USE_GITLAB_WIKI flag
2057-
const tools = USE_GITLAB_WIKI
2097+
let tools = USE_GITLAB_WIKI
20582098
? tools0
20592099
: tools0.filter((tool) => !wikiToolNames.includes(tool.name));
20602100

2101+
// <<< START: Gemini 호환성을 위해 $schema 제거 >>>
2102+
tools = tools.map((tool) => {
2103+
// inputSchema가 존재하고 객체인지 확인
2104+
if (
2105+
tool.inputSchema &&
2106+
typeof tool.inputSchema === "object" &&
2107+
tool.inputSchema !== null
2108+
) {
2109+
// $schema 키가 존재하면 삭제
2110+
if ("$schema" in tool.inputSchema) {
2111+
// 불변성을 위해 새로운 객체 생성 (선택적이지만 권장)
2112+
const modifiedSchema = { ...tool.inputSchema };
2113+
delete modifiedSchema.$schema;
2114+
return { ...tool, inputSchema: modifiedSchema };
2115+
}
2116+
}
2117+
// 변경이 필요 없으면 그대로 반환
2118+
return tool;
2119+
});
2120+
// <<< END: Gemini 호환성을 위해 $schema 제거 >>>
2121+
20612122
return {
2062-
tools,
2123+
tools, // $schema가 제거된 도구 목록 반환
20632124
};
20642125
});
20652126

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@zereight/mcp-gitlab",
3-
"version": "1.0.35",
3+
"version": "1.0.36",
44
"description": "MCP server for using the GitLab API",
55
"license": "MIT",
66
"author": "zereight",

0 commit comments

Comments
 (0)