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.
This commit is contained in:
simple
2025-05-13 02:20:59 +09:00
parent 651072dfd7
commit 08ab1357a0
3 changed files with 72 additions and 6 deletions

5
CHANGELOG.md Normal file
View File

@ -0,0 +1,5 @@
## [Released] - 2025-05-13
### Fixed
- **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.

View File

@ -395,7 +395,8 @@ const allTools = [
},
{
name: "get_repository_tree",
description: "Get the repository tree for a GitLab project (list files and directories)",
description:
"Get the repository tree for a GitLab project (list files and directories)",
inputSchema: zodToJsonSchema(GetRepositoryTreeSchema),
},
];
@ -506,6 +507,7 @@ async function forkProject(
projectId: string,
namespace?: string
): Promise<GitLabFork> {
projectId = decodeURIComponent(projectId); // Decode project ID
const url = new URL(
`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/fork`
);
@ -541,6 +543,7 @@ async function createBranch(
projectId: string,
options: z.infer<typeof CreateBranchOptionsSchema>
): Promise<GitLabReference> {
projectId = decodeURIComponent(projectId); // Decode project ID
const url = new URL(
`${GITLAB_API_URL}/projects/${encodeURIComponent(
projectId
@ -568,6 +571,7 @@ async function createBranch(
* @returns {Promise<string>} The name of the default branch
*/
async function getDefaultBranchRef(projectId: string): Promise<string> {
projectId = decodeURIComponent(projectId); // Decode project ID
const url = new URL(
`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}`
);
@ -595,6 +599,7 @@ async function getFileContents(
filePath: string,
ref?: string
): Promise<GitLabContent> {
projectId = decodeURIComponent(projectId); // Decode project ID
const encodedPath = encodeURIComponent(filePath);
// ref가 없는 경우 default branch를 가져옴
@ -646,6 +651,7 @@ async function createIssue(
projectId: string,
options: z.infer<typeof CreateIssueOptionsSchema>
): Promise<GitLabIssue> {
projectId = decodeURIComponent(projectId); // Decode project ID
const url = new URL(
`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues`
);
@ -685,6 +691,7 @@ async function listIssues(
projectId: string,
options: Omit<z.infer<typeof ListIssuesSchema>, "project_id"> = {}
): Promise<GitLabIssue[]> {
projectId = decodeURIComponent(projectId); // Decode project ID
const url = new URL(
`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues`
);
@ -722,6 +729,7 @@ async function getIssue(
projectId: string,
issueIid: number
): Promise<GitLabIssue> {
projectId = decodeURIComponent(projectId); // Decode project ID
const url = new URL(
`${GITLAB_API_URL}/projects/${encodeURIComponent(
projectId
@ -751,6 +759,7 @@ async function updateIssue(
issueIid: number,
options: Omit<z.infer<typeof UpdateIssueSchema>, "project_id" | "issue_iid">
): Promise<GitLabIssue> {
projectId = decodeURIComponent(projectId); // Decode project ID
const url = new URL(
`${GITLAB_API_URL}/projects/${encodeURIComponent(
projectId
@ -783,6 +792,7 @@ async function updateIssue(
* @returns {Promise<void>}
*/
async function deleteIssue(projectId: string, issueIid: number): Promise<void> {
projectId = decodeURIComponent(projectId); // Decode project ID
const url = new URL(
`${GITLAB_API_URL}/projects/${encodeURIComponent(
projectId
@ -809,6 +819,7 @@ async function listIssueLinks(
projectId: string,
issueIid: number
): Promise<GitLabIssueWithLinkDetails[]> {
projectId = decodeURIComponent(projectId); // Decode project ID
const url = new URL(
`${GITLAB_API_URL}/projects/${encodeURIComponent(
projectId
@ -838,6 +849,7 @@ async function getIssueLink(
issueIid: number,
issueLinkId: number
): Promise<GitLabIssueLink> {
projectId = decodeURIComponent(projectId); // Decode project ID
const url = new URL(
`${GITLAB_API_URL}/projects/${encodeURIComponent(
projectId
@ -871,6 +883,8 @@ async function createIssueLink(
targetIssueIid: number,
linkType: "relates_to" | "blocks" | "is_blocked_by" = "relates_to"
): Promise<GitLabIssueLink> {
projectId = decodeURIComponent(projectId); // Decode project ID
targetProjectId = decodeURIComponent(targetProjectId); // Decode target project ID as well
const url = new URL(
`${GITLAB_API_URL}/projects/${encodeURIComponent(
projectId
@ -906,6 +920,7 @@ async function deleteIssueLink(
issueIid: number,
issueLinkId: number
): Promise<void> {
projectId = decodeURIComponent(projectId); // Decode project ID
const url = new URL(
`${GITLAB_API_URL}/projects/${encodeURIComponent(
projectId
@ -932,6 +947,7 @@ async function createMergeRequest(
projectId: string,
options: z.infer<typeof CreateMergeRequestOptionsSchema>
): Promise<GitLabMergeRequest> {
projectId = decodeURIComponent(projectId); // Decode project ID
const url = new URL(
`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/merge_requests`
);
@ -977,6 +993,7 @@ async function listMergeRequestDiscussions(
projectId: string,
mergeRequestIid: number
): Promise<GitLabDiscussion[]> {
projectId = decodeURIComponent(projectId); // Decode project ID
const url = new URL(
`${GITLAB_API_URL}/projects/${encodeURIComponent(
projectId
@ -1013,6 +1030,7 @@ async function updateMergeRequestNote(
body: string,
resolved?: boolean
): Promise<GitLabDiscussionNote> {
projectId = decodeURIComponent(projectId); // Decode project ID
const url = new URL(
`${GITLAB_API_URL}/projects/${encodeURIComponent(
projectId
@ -1057,6 +1075,7 @@ async function createOrUpdateFile(
last_commit_id?: string,
commit_id?: string
): Promise<GitLabCreateUpdateFileResponse> {
projectId = decodeURIComponent(projectId); // Decode project ID
const encodedPath = encodeURIComponent(filePath);
const url = new URL(
`${GITLAB_API_URL}/projects/${encodeURIComponent(
@ -1139,6 +1158,7 @@ async function createTree(
files: FileOperation[],
ref?: string
): Promise<GitLabTree> {
projectId = decodeURIComponent(projectId); // Decode project ID
const url = new URL(
`${GITLAB_API_URL}/projects/${encodeURIComponent(
projectId
@ -1193,6 +1213,7 @@ async function createCommit(
branch: string,
actions: FileOperation[]
): Promise<GitLabCommit> {
projectId = decodeURIComponent(projectId); // Decode project ID
const url = new URL(
`${GITLAB_API_URL}/projects/${encodeURIComponent(
projectId
@ -1325,6 +1346,7 @@ async function getMergeRequest(
mergeRequestIid?: number,
branchName?: string
): Promise<GitLabMergeRequest> {
projectId = decodeURIComponent(projectId); // Decode project ID
let url: URL;
if (mergeRequestIid) {
@ -1375,6 +1397,7 @@ async function getMergeRequestDiffs(
branchName?: string,
view?: "inline" | "parallel"
): Promise<GitLabMergeRequestDiff[]> {
projectId = decodeURIComponent(projectId); // Decode project ID
if (!mergeRequestIid && !branchName) {
throw new Error("Either mergeRequestIid or branchName must be provided");
}
@ -1426,6 +1449,7 @@ async function updateMergeRequest(
mergeRequestIid?: number,
branchName?: string
): Promise<GitLabMergeRequest> {
projectId = decodeURIComponent(projectId); // Decode project ID
if (!mergeRequestIid && !branchName) {
throw new Error("Either mergeRequestIid or branchName must be provided");
}
@ -1472,6 +1496,7 @@ async function createNote(
noteableIid: number,
body: string
): Promise<any> {
projectId = decodeURIComponent(projectId); // Decode project ID
// ⚙️ 응답 타입은 GitLab API 문서에 따라 조정 가능
const url = new URL(
`${GITLAB_API_URL}/projects/${encodeURIComponent(
@ -1600,6 +1625,7 @@ async function getProject(
with_custom_attributes?: boolean;
} = {}
): Promise<GitLabProject> {
projectId = decodeURIComponent(projectId); // Decode project ID
const url = new URL(
`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}`
);
@ -1674,6 +1700,7 @@ async function listLabels(
projectId: string,
options: Omit<z.infer<typeof ListLabelsSchema>, "project_id"> = {}
): Promise<GitLabLabel[]> {
projectId = decodeURIComponent(projectId); // Decode project ID
// Construct the URL with project path
const url = new URL(
`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/labels`
@ -1716,6 +1743,7 @@ async function getLabel(
labelId: number | string,
includeAncestorGroups?: boolean
): Promise<GitLabLabel> {
projectId = decodeURIComponent(projectId); // Decode project ID
const url = new URL(
`${GITLAB_API_URL}/projects/${encodeURIComponent(
projectId
@ -1754,6 +1782,7 @@ async function createLabel(
projectId: string,
options: Omit<z.infer<typeof CreateLabelSchema>, "project_id">
): Promise<GitLabLabel> {
projectId = decodeURIComponent(projectId); // Decode project ID
// Make the API request
const response = await fetch(
`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/labels`,
@ -1785,6 +1814,7 @@ async function updateLabel(
labelId: number | string,
options: Omit<z.infer<typeof UpdateLabelSchema>, "project_id" | "label_id">
): Promise<GitLabLabel> {
projectId = decodeURIComponent(projectId); // Decode project ID
// Make the API request
const response = await fetch(
`${GITLAB_API_URL}/projects/${encodeURIComponent(
@ -1815,6 +1845,7 @@ async function deleteLabel(
projectId: string,
labelId: number | string
): Promise<void> {
projectId = decodeURIComponent(projectId); // Decode project ID
// Make the API request
const response = await fetch(
`${GITLAB_API_URL}/projects/${encodeURIComponent(
@ -1908,6 +1939,7 @@ async function listWikiPages(
projectId: string,
options: Omit<z.infer<typeof ListWikiPagesSchema>, "project_id"> = {}
): Promise<GitLabWikiPage[]> {
projectId = decodeURIComponent(projectId); // Decode project ID
const url = new URL(
`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/wikis`
);
@ -1929,6 +1961,7 @@ async function getWikiPage(
projectId: string,
slug: string
): Promise<GitLabWikiPage> {
projectId = decodeURIComponent(projectId); // Decode project ID
const response = await fetch(
`${GITLAB_API_URL}/projects/${encodeURIComponent(
projectId
@ -1949,6 +1982,7 @@ async function createWikiPage(
content: string,
format?: string
): Promise<GitLabWikiPage> {
projectId = decodeURIComponent(projectId); // Decode project ID
const body: Record<string, any> = { title, content };
if (format) body.format = format;
const response = await fetch(
@ -1974,6 +2008,7 @@ async function updateWikiPage(
content?: string,
format?: string
): Promise<GitLabWikiPage> {
projectId = decodeURIComponent(projectId); // Decode project ID
const body: Record<string, any> = {};
if (title) body.title = title;
if (content) body.content = content;
@ -1997,6 +2032,7 @@ async function updateWikiPage(
* Delete a wiki page
*/
async function deleteWikiPage(projectId: string, slug: string): Promise<void> {
projectId = decodeURIComponent(projectId); // Decode project ID
const response = await fetch(
`${GITLAB_API_URL}/projects/${encodeURIComponent(
projectId
@ -2018,16 +2054,20 @@ async function deleteWikiPage(projectId: string, slug: string): Promise<void> {
async function getRepositoryTree(
options: GetRepositoryTreeOptions
): Promise<GitLabTreeItem[]> {
options.project_id = decodeURIComponent(options.project_id); // Decode project_id within options
const queryParams = new URLSearchParams();
if (options.path) queryParams.append("path", options.path);
if (options.ref) queryParams.append("ref", options.ref);
if (options.recursive) queryParams.append("recursive", "true");
if (options.per_page) queryParams.append("per_page", options.per_page.toString());
if (options.per_page)
queryParams.append("per_page", options.per_page.toString());
if (options.page_token) queryParams.append("page_token", options.page_token);
if (options.pagination) queryParams.append("pagination", options.pagination);
const response = await fetch(
`${GITLAB_API_URL}/projects/${encodeURIComponent(options.project_id)}/repository/tree?${queryParams.toString()}`,
`${GITLAB_API_URL}/projects/${encodeURIComponent(
options.project_id
)}/repository/tree?${queryParams.toString()}`,
{
headers: {
Authorization: `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
@ -2054,12 +2094,33 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
? allTools.filter((tool) => readOnlyTools.includes(tool.name))
: allTools;
// Toggle wiki tools by USE_GITLAB_WIKI flag
const tools = USE_GITLAB_WIKI
let tools = USE_GITLAB_WIKI
? tools0
: tools0.filter((tool) => !wikiToolNames.includes(tool.name));
// <<< START: Gemini 호환성을 위해 $schema 제거 >>>
tools = tools.map((tool) => {
// inputSchema가 존재하고 객체인지 확인
if (
tool.inputSchema &&
typeof tool.inputSchema === "object" &&
tool.inputSchema !== null
) {
// $schema 키가 존재하면 삭제
if ("$schema" in tool.inputSchema) {
// 불변성을 위해 새로운 객체 생성 (선택적이지만 권장)
const modifiedSchema = { ...tool.inputSchema };
delete modifiedSchema.$schema;
return { ...tool, inputSchema: modifiedSchema };
}
}
// 변경이 필요 없으면 그대로 반환
return tool;
});
// <<< END: Gemini 호환성을 위해 $schema 제거 >>>
return {
tools,
tools, // $schema가 제거된 도구 목록 반환
};
});

View File

@ -1,6 +1,6 @@
{
"name": "@zereight/mcp-gitlab",
"version": "1.0.35",
"version": "1.0.36",
"description": "MCP server for using the GitLab API",
"license": "MIT",
"author": "zereight",