From 6a61a510b92ab24bc6647febc9ec73264be4b0fe Mon Sep 17 00:00:00 2001 From: Admin Date: Mon, 17 Mar 2025 17:34:21 -0700 Subject: [PATCH] Fix URL construction and add smart API URL normalization --- index.ts | 183 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 94 insertions(+), 89 deletions(-) diff --git a/index.ts b/index.ts index ff11901..b1c70fa 100644 --- a/index.ts +++ b/index.ts @@ -11,6 +11,8 @@ import { z } from "zod"; import { zodToJsonSchema } from "zod-to-json-schema"; import { fileURLToPath } from "url"; import { dirname, resolve } from "path"; +import fs from "fs"; +import path from "path"; import { GitLabForkSchema, GitLabReferenceSchema, @@ -54,10 +56,24 @@ import { CreateNoteSchema, } from "./schemas.js"; +// Read version from package.json +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const packageJsonPath = path.resolve(__dirname, '../package.json'); +let SERVER_VERSION = "unknown"; +try { + if (fs.existsSync(packageJsonPath)) { + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + SERVER_VERSION = packageJson.version || SERVER_VERSION; + } +} catch (error) { + console.error("Warning: Could not read version from package.json:", error); +} + const server = new Server( { name: "better-gitlab-mcp-server", - version: "1.0.7-fix", + version: SERVER_VERSION, }, { capabilities: { @@ -67,8 +83,34 @@ const server = new Server( ); const GITLAB_PERSONAL_ACCESS_TOKEN = process.env.GITLAB_PERSONAL_ACCESS_TOKEN; -const GITLAB_API_URL = - process.env.GITLAB_API_URL || "https://gitlab.com/api/v4"; + +// Smart URL handling for GitLab API +function normalizeGitLabApiUrl(url?: string): string { + if (!url) { + return "https://gitlab.com/api/v4"; + } + + // Remove trailing slash if present + let normalizedUrl = url.endsWith('/') ? url.slice(0, -1) : url; + + // Check if URL already has /api/v4 + if (!normalizedUrl.endsWith('/api/v4') && !normalizedUrl.endsWith('/api/v4/')) { + // Append /api/v4 if not already present + normalizedUrl = `${normalizedUrl}/api/v4`; + } + + return normalizedUrl; +} + +// Use the normalizeGitLabApiUrl function to handle various URL formats +const GITLAB_API_URL = normalizeGitLabApiUrl(process.env.GITLAB_API_URL || ""); + +// Add debug logging for API URL construction +console.log("=== MCP Server Configuration ==="); +console.log(`GITLAB_API_URL = "${GITLAB_API_URL}"`); +console.log(`Example project API URL = "${GITLAB_API_URL}/projects/123"`); +console.log(`Example Notes API URL = "${GITLAB_API_URL}/projects/123/issues/1/notes"`); +console.log("==============================="); if (!GITLAB_PERSONAL_ACCESS_TOKEN) { console.error("GITLAB_PERSONAL_ACCESS_TOKEN environment variable is not set"); @@ -101,7 +143,7 @@ async function forkProject( ): Promise { // API 엔드포인트 URL 생성 const url = new URL( - `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(projectId)}/fork` + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/fork` ); if (namespace) { @@ -129,7 +171,7 @@ async function createBranch( options: z.infer ): Promise { const url = new URL( - `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent( + `${GITLAB_API_URL}/projects/${encodeURIComponent( projectId )}/repository/branches` ); @@ -150,7 +192,7 @@ async function createBranch( // 프로젝트의 기본 브랜치 조회 async function getDefaultBranchRef(projectId: string): Promise { const url = new URL( - `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(projectId)}` + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}` ); const response = await fetch(url.toString(), { @@ -176,7 +218,7 @@ async function getFileContents( } const url = new URL( - `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent( + `${GITLAB_API_URL}/projects/${encodeURIComponent( projectId )}/repository/files/${encodedPath}` ); @@ -213,7 +255,7 @@ async function createIssue( options: z.infer ): Promise { const url = new URL( - `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent(projectId)}/issues` + `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues` ); const response = await fetch(url.toString(), { @@ -244,7 +286,7 @@ async function createMergeRequest( options: z.infer ): Promise { const url = new URL( - `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent( + `${GITLAB_API_URL}/projects/${encodeURIComponent( projectId )}/merge_requests` ); @@ -292,7 +334,7 @@ async function createOrUpdateFile( ): Promise { const encodedPath = encodeURIComponent(filePath); const url = new URL( - `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent( + `${GITLAB_API_URL}/projects/${encodeURIComponent( projectId )}/repository/files/${encodedPath}` ); @@ -344,7 +386,7 @@ async function createTree( ref?: string ): Promise { const url = new URL( - `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent( + `${GITLAB_API_URL}/projects/${encodeURIComponent( projectId )}/repository/tree` ); @@ -392,7 +434,7 @@ async function createCommit( actions: FileOperation[] ): Promise { const url = new URL( - `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent( + `${GITLAB_API_URL}/projects/${encodeURIComponent( projectId )}/repository/commits` ); @@ -437,7 +479,7 @@ async function searchProjects( page: number = 1, perPage: number = 20 ): Promise { - const url = new URL(`${GITLAB_API_URL}/api/v4/projects`); + const url = new URL(`${GITLAB_API_URL}/projects`); url.searchParams.append("search", query); url.searchParams.append("page", page.toString()); url.searchParams.append("per_page", perPage.toString()); @@ -477,7 +519,7 @@ async function searchProjects( async function createRepository( options: z.infer ): Promise { - const response = await fetch(`${GITLAB_API_URL}/api/v4/projects`, { + const response = await fetch(`${GITLAB_API_URL}/projects`, { method: "POST", headers: { Accept: "application/json", @@ -511,7 +553,7 @@ async function getMergeRequest( mergeRequestIid: number ): Promise { const url = new URL( - `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent( + `${GITLAB_API_URL}/projects/${encodeURIComponent( projectId )}/merge_requests/${mergeRequestIid}` ); @@ -531,7 +573,7 @@ async function getMergeRequestDiffs( view?: "inline" | "parallel" ): Promise { const url = new URL( - `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent( + `${GITLAB_API_URL}/projects/${encodeURIComponent( projectId )}/merge_requests/${mergeRequestIid}/changes` ); @@ -559,7 +601,7 @@ async function updateMergeRequest( > ): Promise { const url = new URL( - `${GITLAB_API_URL}/api/v4/projects/${encodeURIComponent( + `${GITLAB_API_URL}/projects/${encodeURIComponent( projectId )}/merge_requests/${mergeRequestIid}` ); @@ -582,42 +624,26 @@ async function createNote( body: string ): Promise { // ⚙️ 응답 타입은 GitLab API 문서에 따라 조정 가능 - // Don't add /api/v4 again since it's already included in GITLAB_API_URL - console.error("DEBUG - createNote - GITLAB_API_URL from env: " + process.env.GITLAB_API_URL); - console.error("DEBUG - createNote - GITLAB_API_URL in code: " + GITLAB_API_URL); - const url = new URL( `${GITLAB_API_URL}/projects/${encodeURIComponent( projectId )}/${noteableType}s/${noteableIid}/notes` // Using plural form (issues/merge_requests) as per GitLab API documentation ); - // Add some debug logging - console.error("DEBUG - createNote function called"); - console.error(`DEBUG - createNote - URL: ${url.toString()}`); - console.error(`DEBUG - createNote - projectId: ${projectId}, noteableType: ${noteableType}, noteableIid: ${noteableIid}`); + const response = await fetch(url.toString(), { + method: "POST", + headers: DEFAULT_HEADERS, + body: JSON.stringify({ body }), + }); - try { - const response = await fetch(url.toString(), { - method: "POST", - headers: DEFAULT_HEADERS, - body: JSON.stringify({ body }), - }); - - if (!response.ok) { - const errorText = await response.text(); - console.error(`DEBUG - createNote - Error response: ${response.status} ${response.statusText}`); - console.error(`DEBUG - createNote - Error body: ${errorText}`); - throw new Error( - `GitLab API error: ${response.status} ${response.statusText}\n${errorText}` - ); - } - - return await response.json(); - } catch (error) { - console.error(`DEBUG - createNote - Exception: ${error instanceof Error ? error.message : String(error)}`); - throw error; + if (!response.ok) { + const errorText = await response.text(); + throw new Error( + `GitLab API error: ${response.status} ${response.statusText}\n${errorText}` + ); } + + return await response.json(); } server.setRequestHandler(ListToolsRequestSchema, async () => { @@ -850,45 +876,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { } case "create_note": { - try { - const args = CreateNoteSchema.parse(request.params.arguments); - const { project_id, noteable_type, noteable_iid, body } = args; + const args = CreateNoteSchema.parse(request.params.arguments); + const { project_id, noteable_type, noteable_iid, body } = args; - // Debug info that will be included in the response - const debugInfo = { - gitlab_api_url: GITLAB_API_URL, - project_id, - noteable_type, - noteable_iid, - constructed_url: `${GITLAB_API_URL}/projects/${encodeURIComponent(project_id)}/${noteable_type}s/${noteable_iid}/notes` - }; - - try { - const note = await createNote( - project_id, - noteable_type, - noteable_iid, - body - ); - return { - content: [{ type: "text", text: JSON.stringify(note, null, 2) }], - }; - } catch (error) { - // Include debug info in the error message - throw new Error( - `Error with debug info: ${JSON.stringify(debugInfo)}\n${error instanceof Error ? error.message : String(error)}` - ); - } - } catch (error) { - if (error instanceof z.ZodError) { - throw new Error( - `Invalid arguments: ${error.errors - .map((e) => `${e.path.join(".")}: ${e.message}`) - .join(", ")}` - ); - } - throw error; - } + const note = await createNote( + project_id, + noteable_type, + noteable_iid, + body + ); + return { + content: [{ type: "text", text: JSON.stringify(note, null, 2) }], + }; } default: @@ -907,13 +906,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { }); async function runServer() { - console.error("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); - console.error("!!! RUNNING VERSION 1.0.7-fix WITH URL DEBUGGING FIX !!!"); - console.error("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); + try { + console.error("========================"); + console.error(`GitLab MCP Server v${SERVER_VERSION}`); + console.error(`API URL: ${GITLAB_API_URL}`); + console.error("========================"); - const transport = new StdioServerTransport(); - await server.connect(transport); - console.error("GitLab MCP Server running on stdio"); + const transport = new StdioServerTransport(); + await server.connect(transport); + console.error("GitLab MCP Server running on stdio"); + } catch (error) { + console.error("Error initializing server:", error); + process.exit(1); + } } runServer().catch((error) => {