From d5a567403b1e41b9531e94feb10493796d6d850a Mon Sep 17 00:00:00 2001 From: Ismar Iljazovic Date: Tue, 30 Dec 2025 11:08:38 +0100 Subject: [PATCH] fix: use workflow_run trigger for npm-publish (GITHUB_TOKEN releases don't emit events) --- .beads/metadata.json | 2 +- .github/workflows/auto-merge.yml | 2 +- .github/workflows/npm-publish.yml | 7 +++-- .pre-commit-config.yaml | 1 + .typos.toml | 6 ++++ AGENTS.md | 1 - CHANGELOG.md | 14 ++++----- LICENSE | 2 +- docs/dynamic-api-url.md | 4 +-- test/client-pool-test.ts | 18 +++++------ test/clients/client.ts | 2 +- test/clients/custom-header-client.ts | 7 ++--- test/clients/sse-client.ts | 2 +- test/clients/stdio-client.ts | 6 ++-- test/clients/streamable-http-client.ts | 2 +- test/dynamic-api-url-test.ts | 30 +++++++++---------- test/dynamic-routing-tests.ts | 18 +++++------ test/multi-server-test.ts | 12 ++++---- test/readonly-mcp-tests.ts | 38 ++++++++++++------------ test/remote-auth-simple-test.ts | 29 +++++++++--------- test/remote-auth-tests.ts | 41 +++++++++++++------------- test/test-all-transport-server.ts | 36 +++++++++++----------- test/utils/mock-gitlab-server.ts | 13 ++++---- test/utils/server-launcher.ts | 34 ++++++++++----------- 24 files changed, 166 insertions(+), 161 deletions(-) create mode 100644 .typos.toml diff --git a/.beads/metadata.json b/.beads/metadata.json index c787975e..f581edc0 100644 --- a/.beads/metadata.json +++ b/.beads/metadata.json @@ -1,4 +1,4 @@ { "database": "beads.db", "jsonl_export": "issues.jsonl" -} \ No newline at end of file +} diff --git a/.github/workflows/auto-merge.yml b/.github/workflows/auto-merge.yml index 799373a8..3cfa5908 100644 --- a/.github/workflows/auto-merge.yml +++ b/.github/workflows/auto-merge.yml @@ -11,4 +11,4 @@ permissions: jobs: auto-merge: uses: detailobsessed/ci-components/.github/workflows/auto-merge-dependabot.yml@main - secrets: inherit \ No newline at end of file + secrets: inherit diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index 09d74353..aaa96b3d 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -1,8 +1,10 @@ name: NPM Publish on: - release: - types: [published] + workflow_run: + workflows: ["Release"] + branches: [main] + types: [completed] workflow_dispatch: permissions: @@ -11,4 +13,5 @@ permissions: jobs: publish: + if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }} uses: detailobsessed/ci-components/.github/workflows/npm-publish-bun.yml@main diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 391623fe..eb66f77d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,6 +30,7 @@ repos: hooks: - id: typos priority: 1 + exclude: ^CHANGELOG\.md$ - repo: local hooks: diff --git a/.typos.toml b/.typos.toml new file mode 100644 index 00000000..18c9eb76 --- /dev/null +++ b/.typos.toml @@ -0,0 +1,6 @@ +[files] +extend-exclude = ["CHANGELOG.md", "*.lock"] + +[default.extend-words] +# Git commit hashes that look like typos +ba = "ba" diff --git a/AGENTS.md b/AGENTS.md index df7a4af9..1a9b0313 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -37,4 +37,3 @@ bd sync # Sync with git - NEVER stop before pushing - that leaves work stranded locally - NEVER say "ready to push when you are" - YOU must push - If push fails, resolve and retry until it succeeds - diff --git a/CHANGELOG.md b/CHANGELOG.md index aea17984..68a2de08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -98,8 +98,8 @@ * add branch comparison functionality and update related schemas ([af81bd4](https://github.com/detailobsessed/efficient-gitlab-mcp/commit/af81bd402aeff1a6f03afcf7853d83d89231a8fe)) * add configuration files and scripts for project setup ✨ ([5b35bc1](https://github.com/detailobsessed/efficient-gitlab-mcp/commit/5b35bc163c3277523fbf264523601f55103d714b)) * add configuration files and scripts for project setup ✨ ([4aac7f5](https://github.com/detailobsessed/efficient-gitlab-mcp/commit/4aac7f576a91b14fcf7d379c5baa13df3762ef86)) -* add cookie-based authentication support for enterprise GitLab ([#101](https://github.com/detailobsessed/efficient-gitlab-mcp/issues/101)) ([402f068](https://github.com/detailobsessed/efficient-gitlab-mcp/commit/402f06847056903058a5bf5bed0b65d81e0c5757)), closes [#100](https://github.com/detailobsessed/efficient-gitlab-mcp/issues/100) [tou#cookie](https://github.com/tou/issues/cookie) [tou#cookie](https://github.com/tou/issues/cookie) [tou#cookie](https://github.com/tou/issues/cookie) -* add cookie-based authentication support for enterprise GitLab ([#101](https://github.com/detailobsessed/efficient-gitlab-mcp/issues/101)) ([17b8574](https://github.com/detailobsessed/efficient-gitlab-mcp/commit/17b85746b5c3d9fa64f7c912dbefc7fa1184c59d)), closes [#100](https://github.com/detailobsessed/efficient-gitlab-mcp/issues/100) [tou#cookie](https://github.com/tou/issues/cookie) [tou#cookie](https://github.com/tou/issues/cookie) [tou#cookie](https://github.com/tou/issues/cookie) +* add cookie-based authentication support for enterprise GitLab ([#101](https://github.com/detailobsessed/efficient-gitlab-mcp/issues/101)) ([402f068](https://github.com/detailobsessed/efficient-gitlab-mcp/commit/402f06847056903058a5bf5bed0b65d81e0c5757)), closes [#100](https://github.com/detailobsessed/efficient-gitlab-mcp/issues/100) [you#cookie](https://github.com/tou/issues/cookie) [you#cookie](https://github.com/tou/issues/cookie) [you#cookie](https://github.com/tou/issues/cookie) +* add cookie-based authentication support for enterprise GitLab ([#101](https://github.com/detailobsessed/efficient-gitlab-mcp/issues/101)) ([17b8574](https://github.com/detailobsessed/efficient-gitlab-mcp/commit/17b85746b5c3d9fa64f7c912dbefc7fa1184c59d)), closes [#100](https://github.com/detailobsessed/efficient-gitlab-mcp/issues/100) [you#cookie](https://github.com/tou/issues/cookie) [you#cookie](https://github.com/tou/issues/cookie) [you#cookie](https://github.com/tou/issues/cookie) * Add create_merge_request_thread tool for diff notes ([026dd58](https://github.com/detailobsessed/efficient-gitlab-mcp/commit/026dd58887079bb60187d6acacaafc6fa28d0c3d)) * Add create_merge_request_thread tool for diff notes ([23b0348](https://github.com/detailobsessed/efficient-gitlab-mcp/commit/23b03481eacc2b32a1f4afdf5a125ca23f87bdcf)) * Add createDraftNote api support, useful for bulk code review ([5f08153](https://github.com/detailobsessed/efficient-gitlab-mcp/commit/5f08153da675a6fbec780329c82c6a3395f3f691)) @@ -294,7 +294,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). > 20 September 2025 - Fix/inline schemas [`#242`](https://github.com/zereight/gitlab-mcp/pull/242) -- Add "full_diff" parameter to get_commit_diff so we can retreive more than 20 files [`#244`](https://github.com/zereight/gitlab-mcp/pull/244) +- Add "full_diff" parameter to get_commit_diff so we can retrieve more than 20 files [`#244`](https://github.com/zereight/gitlab-mcp/pull/244) - refactor(schemas): replace custom boolean validators with native zod boolean [`#245`](https://github.com/zereight/gitlab-mcp/pull/245) - Feat/add delete to streamhttp [`#246`](https://github.com/zereight/gitlab-mcp/pull/246) - Fix: Add 'logic' and 'style' line types to enum for mr_discussions [`#248`](https://github.com/zereight/gitlab-mcp/pull/248) @@ -323,7 +323,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - feat: Add NPM publish workflow for automated package publishing [`#208`](https://github.com/zereight/gitlab-mcp/pull/208) - Fix list of tools in `README.md` [`#205`](https://github.com/zereight/gitlab-mcp/pull/205) - FIX: flexible boolean [`#201`](https://github.com/zereight/gitlab-mcp/pull/201) -- feat(attachement):download attachement, e.g. images [`#200`](https://github.com/zereight/gitlab-mcp/pull/200) +- feat(attachment):download attachment, e.g. images [`#200`](https://github.com/zereight/gitlab-mcp/pull/200) - FEAT: merge MR [`#193`](https://github.com/zereight/gitlab-mcp/pull/193) - FEAT: get draft note [`#197`](https://github.com/zereight/gitlab-mcp/pull/197) - feat: Add createDraftNote api support, useful for bulk code review [`#183`](https://github.com/zereight/gitlab-mcp/pull/183) @@ -371,7 +371,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - feat: add pagination support for CI job logs to prevent context window flooding [`#97`](https://github.com/zereight/gitlab-mcp/pull/97) - FIX: private token auth [`#91`](https://github.com/zereight/gitlab-mcp/pull/91) - FEAT: private token auth [`#89`](https://github.com/zereight/gitlab-mcp/pull/89) -- FIX: list issues assginee username [`#87`](https://github.com/zereight/gitlab-mcp/pull/87) +- FIX: list issues assignee username [`#87`](https://github.com/zereight/gitlab-mcp/pull/87) - FEAT: add support for `remove_source_branch` and `squash` options for merge requests [`#86`](https://github.com/zereight/gitlab-mcp/pull/86) - Fix for null error [`#85`](https://github.com/zereight/gitlab-mcp/pull/85) - FIX: bug get issues [`#83`](https://github.com/zereight/gitlab-mcp/pull/83) @@ -474,7 +474,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - feat: Add NPM publish workflow for automated package publishing [`#208`](https://github.com/zereight/gitlab-mcp/pull/208) - Fix list of tools in `README.md` [`#205`](https://github.com/zereight/gitlab-mcp/pull/205) - FIX: flexible boolean [`#201`](https://github.com/zereight/gitlab-mcp/pull/201) -- feat(attachement):download attachement, e.g. images [`#200`](https://github.com/zereight/gitlab-mcp/pull/200) +- feat(attachment):download attachment, e.g. images [`#200`](https://github.com/zereight/gitlab-mcp/pull/200) - FEAT: merge MR [`#193`](https://github.com/zereight/gitlab-mcp/pull/193) - FEAT: get draft note [`#197`](https://github.com/zereight/gitlab-mcp/pull/197) - feat: Add createDraftNote api support, useful for bulk code review [`#183`](https://github.com/zereight/gitlab-mcp/pull/183) @@ -600,7 +600,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). > 7 June 2025 -- FIX: list issues assginee username [`#87`](https://github.com/zereight/gitlab-mcp/pull/87) +- FIX: list issues assignee username [`#87`](https://github.com/zereight/gitlab-mcp/pull/87) - FEAT: add support for `remove_source_branch` and `squash` options for merge requests [`#86`](https://github.com/zereight/gitlab-mcp/pull/86) #### [v1.0.59](https://github.com/zereight/gitlab-mcp/compare/v1.0.57...v1.0.59) diff --git a/LICENSE b/LICENSE index 5704a885..bbefa3d9 100644 --- a/LICENSE +++ b/LICENSE @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +SOFTWARE. diff --git a/docs/dynamic-api-url.md b/docs/dynamic-api-url.md index ede3bd93..aa70a7ce 100644 --- a/docs/dynamic-api-url.md +++ b/docs/dynamic-api-url.md @@ -92,7 +92,7 @@ X-GitLab-API-URL: https://gitlab.example.com | `X-GitLab-API-URL` | Yes* | Full URL to GitLab API (e.g., `https://gitlab.example.com`) | | `MCP-Session-ID` | Yes** | Session identifier for maintaining state | -\* Required when `ENABLE_DYNAMIC_API_URL=true` +\* Required when `ENABLE_DYNAMIC_API_URL=true` \** Required for subsequent requests in the same session ### URL Format @@ -391,4 +391,4 @@ Specifies the GitLab API URL for the current request. ## License -This feature is part of the GitLab MCP Server and follows the same license terms. \ No newline at end of file +This feature is part of the GitLab MCP Server and follows the same license terms. diff --git a/test/client-pool-test.ts b/test/client-pool-test.ts index 8e90c441..8d1f18e8 100644 --- a/test/client-pool-test.ts +++ b/test/client-pool-test.ts @@ -5,13 +5,13 @@ import { describe, test, after, before } from 'node:test'; import assert from 'node:assert'; -import { - launchServer, - findAvailablePort, - cleanupServers, - ServerInstance, +import { + launchServer, + findAvailablePort, + cleanupServers, + ServerInstance, TransportMode, - HOST + HOST } from './utils/server-launcher.js'; import { MockGitLabServer, findMockServerPort } from './utils/mock-gitlab-server.js'; import { CustomHeaderClient } from './clients/custom-header-client.js'; @@ -115,17 +115,17 @@ describe('Client Pool Limits', () => { 'x-gitlab-api-url': 'https://gitlab-3.example.com/api/v4' }); await client3.connect(mcpUrl); - + try { await client3.callTool('list_projects', { per_page: 1 }); assert.fail('Request 3 should have failed with pool limit error'); } catch (error: any) { console.log(' ℹ️ Error received:', error.message); assert.ok( - error.message.includes('capacity reached') || error.message.includes('pool is full'), + error.message.includes('capacity reached') || error.message.includes('pool is full'), 'Error should be about server capacity' ); } await client3.disconnect(); }); -}); \ No newline at end of file +}); diff --git a/test/clients/client.ts b/test/clients/client.ts index 5f76c511..3c4e0554 100644 --- a/test/clients/client.ts +++ b/test/clients/client.ts @@ -64,4 +64,4 @@ export class MCPToolCallError extends MCPClientError { super(message, cause); this.name = 'MCPToolCallError'; } -} +} diff --git a/test/clients/custom-header-client.ts b/test/clients/custom-header-client.ts index 0ce81e7e..f1f38129 100644 --- a/test/clients/custom-header-client.ts +++ b/test/clients/custom-header-client.ts @@ -30,9 +30,9 @@ export class CustomHeaderClient { const opts = options as CustomHeaderClientOptions; this.customHeaders = opts.headers || {}; this.timeout = opts.timeout || 30000; - this.client = new Client({ - name: opts.clientName || "test-client-with-headers", - version: opts.clientVersion || "1.0.0" + this.client = new Client({ + name: opts.clientName || "test-client-with-headers", + version: opts.clientVersion || "1.0.0" }); } else { // Backward compatible: treat options as headers record @@ -141,4 +141,3 @@ export class CustomHeaderClient { return this.transport !== null; } } - diff --git a/test/clients/sse-client.ts b/test/clients/sse-client.ts index 1b86397d..3ca0e7e3 100644 --- a/test/clients/sse-client.ts +++ b/test/clients/sse-client.ts @@ -110,4 +110,4 @@ export class SSETestClient implements MCPClientInterface { get isConnected(): boolean { return this.transport !== null; } -} \ No newline at end of file +} diff --git a/test/clients/stdio-client.ts b/test/clients/stdio-client.ts index 41605f33..6291e23b 100644 --- a/test/clients/stdio-client.ts +++ b/test/clients/stdio-client.ts @@ -29,14 +29,14 @@ export class StdioTestClient implements MCPClientInterface { // Prepare environment variables for the server process const serverEnv: Record = {}; - + // Copy process.env, filtering out undefined values for (const [key, value] of Object.entries(process.env)) { if (value !== undefined) { serverEnv[key] = value; } } - + // Add custom environment variables if (env) { Object.assign(serverEnv, env); @@ -133,4 +133,4 @@ export class StdioTestClient implements MCPClientInterface { get isConnected(): boolean { return this.transport !== null; } -} \ No newline at end of file +} diff --git a/test/clients/streamable-http-client.ts b/test/clients/streamable-http-client.ts index 390f8df3..f8d7cf5a 100644 --- a/test/clients/streamable-http-client.ts +++ b/test/clients/streamable-http-client.ts @@ -110,4 +110,4 @@ export class StreamableHTTPTestClient implements MCPClientInterface { get isConnected(): boolean { return this.transport !== null; } -} \ No newline at end of file +} diff --git a/test/dynamic-api-url-test.ts b/test/dynamic-api-url-test.ts index f7175e71..15ff809d 100644 --- a/test/dynamic-api-url-test.ts +++ b/test/dynamic-api-url-test.ts @@ -5,13 +5,13 @@ import { describe, test, after, before } from 'node:test'; import assert from 'node:assert'; -import { - launchServer, - findAvailablePort, - cleanupServers, - ServerInstance, +import { + launchServer, + findAvailablePort, + cleanupServers, + ServerInstance, TransportMode, - HOST + HOST } from './utils/server-launcher.js'; import { MockGitLabServer, findMockServerPort } from './utils/mock-gitlab-server.js'; import { CustomHeaderClient } from './clients/custom-header-client.js'; @@ -94,10 +94,10 @@ describe('Dynamic API URL - Multiple GitLab Instances', () => { await client.connect(mcpUrl); const tools = await client.listTools(); - + assert.ok(tools.tools.length > 0, 'Should have tools'); console.log(` ✓ Connected to instance 1, got ${tools.tools.length} tools`); - + await client.disconnect(); }); @@ -109,10 +109,10 @@ describe('Dynamic API URL - Multiple GitLab Instances', () => { await client.connect(mcpUrl); const tools = await client.listTools(); - + assert.ok(tools.tools.length > 0, 'Should have tools'); console.log(` ✓ Connected to instance 2, got ${tools.tools.length} tools`); - + await client.disconnect(); }); @@ -231,10 +231,10 @@ describe('Dynamic API URL - Multiple GitLab Instances', () => { await client.connect(mcpUrl); const tools = await client.listTools(); - + assert.ok(tools.tools.length > 0, 'Should work with normalized URL'); console.log(' ✓ URL normalization works correctly'); - + await client.disconnect(); }); }); @@ -318,12 +318,12 @@ describe('Dynamic API URL - Connection Pool', () => { // Check metrics const response = await fetch(metricsUrl); assert.ok(response.ok, 'Metrics endpoint should be accessible'); - + const metrics = await response.json(); assert.ok(metrics.gitlabClientPool, 'Should have pool metrics'); assert.ok(typeof metrics.gitlabClientPool.size === 'number', 'Should have pool size'); assert.ok(typeof metrics.gitlabClientPool.maxSize === 'number', 'Should have max size'); - + console.log(' ✓ Pool metrics available'); console.log(` ℹ️ Pool size: ${metrics.gitlabClientPool.size}/${metrics.gitlabClientPool.maxSize}`); @@ -364,4 +364,4 @@ describe('Dynamic API URL - Connection Pool', () => { await client.disconnect(); } }); -}); \ No newline at end of file +}); diff --git a/test/dynamic-routing-tests.ts b/test/dynamic-routing-tests.ts index 65c6014e..3e84085b 100644 --- a/test/dynamic-routing-tests.ts +++ b/test/dynamic-routing-tests.ts @@ -1,11 +1,11 @@ import { describe, test, after, before } from 'node:test'; import assert from 'node:assert'; -import { - launchServer, - findAvailablePort, - ServerInstance, +import { + launchServer, + findAvailablePort, + ServerInstance, TransportMode, - HOST + HOST } from './utils/server-launcher.js'; import { MockGitLabServer, findMockServerPort } from './utils/mock-gitlab-server.js'; import { CustomHeaderClient } from './clients/custom-header-client.js'; @@ -257,7 +257,7 @@ describe('Dynamic Routing and Authentication Scenarios', () => { } }); await client.connect(mcpUrl); - + await validateToolCalls(client, headerMockServer, MOCK_TOKEN_HEADER); await client.disconnect(); @@ -416,7 +416,7 @@ async function validateToolCalls(client: CustomHeaderClient, mockServer: MockGit for (const tool of toolsToTest) { mockServer.clearCustomHandlers(); - + let mockPath = ''; let mockResponse: any; @@ -468,7 +468,7 @@ async function validateToolCalls(client: CustomHeaderClient, mockServer: MockGit const result = await client.callTool(tool.name, tool.params); const resultContent = JSON.parse((result.content[0] as any).text); - + // Basic validation that we got the expected object back if (Array.isArray(mockResponse)) { assert.ok(Array.isArray(resultContent)); @@ -484,4 +484,4 @@ async function validateToolCalls(client: CustomHeaderClient, mockServer: MockGit } } } -} \ No newline at end of file +} diff --git a/test/multi-server-test.ts b/test/multi-server-test.ts index aa8752de..f6c637cc 100644 --- a/test/multi-server-test.ts +++ b/test/multi-server-test.ts @@ -1,11 +1,11 @@ import { describe, test, after, before } from 'node:test'; import assert from 'node:assert'; -import { - launchServer, - findAvailablePort, - ServerInstance, +import { + launchServer, + findAvailablePort, + ServerInstance, TransportMode, - HOST + HOST } from './utils/server-launcher.js'; import { MockGitLabServer, findMockServerPort } from './utils/mock-gitlab-server.js'; import { CustomHeaderClient } from './clients/custom-header-client.js'; @@ -195,4 +195,4 @@ describe("Dynamic Client Mode (ENABLE_DYNAMIC_API_URL=true)", () => { } ); }); -}); \ No newline at end of file +}); diff --git a/test/readonly-mcp-tests.ts b/test/readonly-mcp-tests.ts index 19bc3316..d01af572 100644 --- a/test/readonly-mcp-tests.ts +++ b/test/readonly-mcp-tests.ts @@ -64,14 +64,14 @@ const mcpTools: MCPTool[] = [ { name: 'get_project', category: 'project', required: true }, { name: 'list_project_members', category: 'project', required: true }, { name: 'list_group_projects', category: 'project', required: false }, - + // 이슈 관련 { name: 'list_issues', category: 'issue', required: true }, { name: 'my_issues', category: 'issue', required: false }, { name: 'get_issue', category: 'issue', required: true }, { name: 'list_issue_discussions', category: 'issue', required: true }, { name: 'list_issue_links', category: 'issue', required: true }, - + // 머지 리퀘스트 관련 { name: 'list_merge_requests', category: 'merge_request', required: true }, { name: 'get_merge_request', category: 'merge_request', required: true }, @@ -79,7 +79,7 @@ const mcpTools: MCPTool[] = [ { name: 'list_merge_request_diffs', category: 'merge_request', required: true }, { name: 'get_branch_diffs', category: 'merge_request', required: true }, { name: 'mr_discussions', category: 'merge_request', required: true }, - + // 파이프라인 관련 { name: 'list_pipelines', category: 'pipeline', required: true }, { name: 'get_pipeline', category: 'pipeline', required: true }, @@ -87,40 +87,40 @@ const mcpTools: MCPTool[] = [ { name: 'list_pipeline_trigger_jobs', category: 'pipeline', required: true }, { name: 'get_pipeline_job', category: 'pipeline', required: true }, { name: 'get_pipeline_job_output', category: 'pipeline', required: true }, - + // 파일 관리 { name: 'get_file_contents', category: 'file', required: true }, { name: 'get_repository_tree', category: 'file', required: true }, - + // 커밋 관련 { name: 'list_commits', category: 'commit', required: true }, { name: 'get_commit', category: 'commit', required: true }, { name: 'get_commit_diff', category: 'commit', required: true }, - + // 라벨 관련 { name: 'list_labels', category: 'label', required: true }, { name: 'get_label', category: 'label', required: true }, - + // 네임스페이스 관련 { name: 'list_namespaces', category: 'namespace', required: false }, { name: 'get_namespace', category: 'namespace', required: false }, { name: 'verify_namespace', category: 'namespace', required: false }, - + // 사용자 관련 { name: 'get_users', category: 'user', required: false }, - + // 이벤트 관련 { name: 'list_events', category: 'event', required: false }, { name: 'get_project_events', category: 'event', required: true }, - + // 마일스톤 관련 (선택적) { name: 'list_milestones', category: 'milestone', required: true }, { name: 'get_milestone', category: 'milestone', required: true }, - + // 위키 관련 (선택적) { name: 'list_wiki_pages', category: 'wiki', required: true }, { name: 'get_wiki_page', category: 'wiki', required: true }, - + // 그룹 이터레이션 관련 { name: 'list_group_iterations', category: 'iteration', required: false } ]; @@ -199,7 +199,7 @@ async function callMCPTool(toolName: string, parameters: Record = { // 도구별 파라미터 설정 함수 async function setupToolParameters(tool: MCPTool): Promise> { let parameters: Record = {}; - + if (tool.required && TEST_PROJECT_ID) { parameters.project_id = TEST_PROJECT_ID; } @@ -317,21 +317,21 @@ async function testTool(tool: MCPTool): Promise { try { console.log(`🧪 Testing ${tool.name}...`); - + const parameters = await setupToolParameters(tool); const response = await callMCPTool(tool.name, parameters); - + result.response = response; result.status = 'passed'; result.duration = Date.now() - startTime; - + console.log(`✅ ${tool.name} - PASSED (${result.duration}ms)`); - + } catch (error) { result.status = 'failed'; result.error = (error as Error).message; result.duration = Date.now() - startTime; - + console.log(`❌ ${tool.name} - FAILED (${result.duration}ms)`); console.log(` Error: ${(error as Error).message}`); } @@ -371,7 +371,7 @@ async function runReadOnlyTests(): Promise { const result = await testTool(tool); testResults.details.push(result); testResults.total++; - + if (result.status === 'passed') { testResults.passed++; } else if (result.status === 'failed') { diff --git a/test/remote-auth-simple-test.ts b/test/remote-auth-simple-test.ts index 5c4b4f65..ddc3d7f3 100644 --- a/test/remote-auth-simple-test.ts +++ b/test/remote-auth-simple-test.ts @@ -5,13 +5,13 @@ import { describe, test, after, before } from 'node:test'; import assert from 'node:assert'; -import { - launchServer, - findAvailablePort, - cleanupServers, - ServerInstance, +import { + launchServer, + findAvailablePort, + cleanupServers, + ServerInstance, TransportMode, - HOST + HOST } from './utils/server-launcher.js'; import { MockGitLabServer, findMockServerPort } from './utils/mock-gitlab-server.js'; import { CustomHeaderClient } from './clients/custom-header-client.js'; @@ -85,10 +85,10 @@ describe('Remote Authorization - Basic Functionality', () => { await client.connect(mcpUrl); const tools = await client.listTools(); - + assert.ok(tools.tools.length > 0, 'Should have tools'); console.log(` ✓ Connected successfully, got ${tools.tools.length} tools`); - + await client.disconnect(); }); @@ -99,10 +99,10 @@ describe('Remote Authorization - Basic Functionality', () => { await client.connect(mcpUrl); const tools = await client.listTools(); - + assert.ok(tools.tools.length > 0, 'Should have tools'); console.log(` ✓ Connected with Private-Token, got ${tools.tools.length} tools`); - + await client.disconnect(); }); @@ -112,17 +112,17 @@ describe('Remote Authorization - Basic Functionality', () => { }); await client.connect(mcpUrl); - + // List tools multiple times to verify auth persists const tools1 = await client.listTools(); const tools2 = await client.listTools(); const tools3 = await client.listTools(); - + assert.ok(tools1.tools.length > 0, 'Should have tools'); assert.strictEqual(tools1.tools.length, tools2.tools.length, 'Tool count should be consistent'); assert.strictEqual(tools2.tools.length, tools3.tools.length, 'Tool count should be consistent'); console.log(' ✓ Multiple tool list calls successful with persistent auth'); - + await client.disconnect(); }); @@ -170,7 +170,7 @@ describe('Remote Authorization - Session Timeout', () => { }); servers.push(server); mcpUrl = `http://${HOST}:${mcpPort}/mcp`; - + console.log(`Session timeout: ${SESSION_TIMEOUT_SECONDS} seconds`); }); @@ -260,4 +260,3 @@ describe('Remote Authorization - Session Timeout', () => { await clientWithAuth.disconnect(); }); }); - diff --git a/test/remote-auth-tests.ts b/test/remote-auth-tests.ts index 7b2db73f..79e97e4b 100644 --- a/test/remote-auth-tests.ts +++ b/test/remote-auth-tests.ts @@ -7,14 +7,14 @@ import * as path from 'path'; import { describe, test, after, before } from 'node:test'; import assert from 'node:assert'; // Using native fetch (available in Bun and Node 18+) -import { - launchServer, - findAvailablePort, - cleanupServers, - ServerInstance, - TransportMode, - checkHealthEndpoint, - HOST +import { + launchServer, + findAvailablePort, + cleanupServers, + ServerInstance, + TransportMode, + checkHealthEndpoint, + HOST } from './utils/server-launcher.js'; console.log('🔐 Remote Authorization Tests'); @@ -106,16 +106,16 @@ describe('Remote Authorization - Streamable HTTP with Authorization header', () }); servers.push(server); mcpUrl = `http://${HOST}:${port}/mcp`; - + // Verify server started successfully assert.ok(server.process.pid !== undefined, 'Server process should have PID'); - + // Verify health check if (server.port) { const health = await checkHealthEndpoint(server.port); assert.strictEqual(health.status, 'healthy', 'Health status should be healthy'); } - + console.log('Server started with remote authorization enabled'); }); @@ -149,7 +149,7 @@ describe('Remote Authorization - Streamable HTTP with Authorization header', () test('should accept request with Authorization Bearer header', async () => { const sessionId = `test-session-bearer-${Date.now()}`; - + const initRequest = { jsonrpc: '2.0', id: 1, @@ -173,7 +173,7 @@ describe('Remote Authorization - Streamable HTTP with Authorization header', () test('should reuse auth from first request in subsequent requests', async () => { const sessionId = `test-session-reuse-${Date.now()}`; - + // First request with auth const initRequest = { jsonrpc: '2.0', @@ -213,7 +213,7 @@ describe('Remote Authorization - Streamable HTTP with Authorization header', () test('should call tool with Bearer token', async () => { const sessionId = `test-session-tool-${Date.now()}`; - + // Initialize const initRequest = { jsonrpc: '2.0', @@ -252,7 +252,7 @@ describe('Remote Authorization - Streamable HTTP with Authorization header', () assert.ok(toolResponse.result.content, 'Should have content'); assert.ok(Array.isArray(toolResponse.result.content), 'Content should be array'); assert.ok(toolResponse.result.content.length > 0, 'Should have content items'); - + const projectData = JSON.parse(toolResponse.result.content[0].text); assert.ok(projectData.id, 'Should have project id'); assert.ok(projectData.name, 'Should have project name'); @@ -280,7 +280,7 @@ describe('Remote Authorization - Streamable HTTP with Private-Token header', () }); servers.push(server); mcpUrl = `http://${HOST}:${port}/mcp`; - + console.log('Server started for Private-Token tests'); }); @@ -291,7 +291,7 @@ describe('Remote Authorization - Streamable HTTP with Private-Token header', () test('should accept request with Private-Token header', async () => { const sessionId = `test-session-private-${Date.now()}`; - + const initRequest = { jsonrpc: '2.0', id: 1, @@ -314,7 +314,7 @@ describe('Remote Authorization - Streamable HTTP with Private-Token header', () test('should call tool with Private-Token', async () => { const sessionId = `test-session-private-tool-${Date.now()}`; - + // Initialize const initRequest = { jsonrpc: '2.0', @@ -357,7 +357,7 @@ describe('Remote Authorization - Streamable HTTP with Private-Token header', () describe('Remote Authorization - SSE mode should be disabled', () => { test('should fail to start with SSE and REMOTE_AUTHORIZATION', async () => { const port = await findAvailablePort(); - + try { const server = await launchServer({ mode: TransportMode.SSE, @@ -369,7 +369,7 @@ describe('Remote Authorization - SSE mode should be disabled', () => { GITLAB_API_URL: `${GITLAB_API_URL}/api/v4`, } }); - + // If we get here, the server started when it shouldn't have servers.push(server); assert.fail('Server should not start with SSE and REMOTE_AUTHORIZATION=true'); @@ -380,4 +380,3 @@ describe('Remote Authorization - SSE mode should be disabled', () => { } }); }); - diff --git a/test/test-all-transport-server.ts b/test/test-all-transport-server.ts index a398c4be..80fcc9fe 100644 --- a/test/test-all-transport-server.ts +++ b/test/test-all-transport-server.ts @@ -88,18 +88,18 @@ describe('GitLab MCP Server - Stdio Transport', () => { assert.ok(tools !== null && tools !== undefined, 'Tools response should be defined'); assert.ok('tools' in tools, 'Response should have tools property'); assert.ok(Array.isArray(tools.tools) && tools.tools.length > 0, 'Tools array should not be empty'); - + // Check for specific GitLab tools with proper typing const toolNames = tools.tools.map(tool => tool.name); assert.ok(toolNames.includes('list_merge_requests'), 'Should have list_merge_requests tool'); assert.ok(toolNames.includes('get_project'), 'Should have get_project tool'); - + // Verify tools have proper structure - const gitlabTools = tools.tools.filter(tool => + const gitlabTools = tools.tools.filter(tool => tool.name === 'list_merge_requests' || tool.name === 'get_project' ); assert.ok(gitlabTools.length >= 2, 'Should have at least 2 GitLab tools'); - + for (const tool of gitlabTools) { assert.ok(tool.description !== null && tool.description !== undefined, `Tool ${tool.name} should have description`); assert.ok('inputSchema' in tool, `Tool ${tool.name} should have input schema`); @@ -110,7 +110,7 @@ describe('GitLab MCP Server - Stdio Transport', () => { const result = await client.callTool('list_merge_requests', { project_id: TEST_PROJECT_ID }); - + assert.ok(result !== null && result !== undefined, 'Tool call result should be defined'); assert.ok('content' in result, 'Result should have content property'); }); @@ -119,20 +119,20 @@ describe('GitLab MCP Server - Stdio Transport', () => { const result = await client.callTool('get_project', { project_id: TEST_PROJECT_ID }); - + // Verify proper CallToolResult structure assert.ok(result !== null && result !== undefined, 'Tool call result should be defined'); assert.ok('content' in result, 'Result should have content property'); assert.ok(Array.isArray(result.content), 'Content should be an array'); assert.ok(result.content.length > 0, 'Content array should not be empty'); - + // Check content structure const firstContent = result.content[0]; assert.ok(firstContent !== null && firstContent !== undefined, 'First content item should be defined'); assert.ok('type' in firstContent, 'Content item should have type'); assert.strictEqual(firstContent.type, 'text', 'Content type should be text'); assert.ok('text' in firstContent, 'Text content should have text property'); - + // Verify it's valid JSON containing project info const projectData = JSON.parse((firstContent as any).text); assert.ok(projectData !== null && projectData !== undefined, 'Project data should be parseable JSON'); @@ -158,18 +158,18 @@ describe('GitLab MCP Server - SSE Transport', () => { } }); servers.push(server); - + // Verify server started successfully assert.ok(server.process.pid !== undefined, 'Server process should have PID'); assert.strictEqual(server.mode, TransportMode.SSE, 'Server mode should be SSE'); assert.strictEqual(server.port, port, 'Server should use correct port'); - + // Verify health check const health = await checkHealthEndpoint(server.port); assert.strictEqual(health.status, 'healthy', 'Health status should be healthy'); assert.strictEqual(health.transport, 'sse', 'Transport should be SSE'); assert.ok(health.version !== null && health.version !== undefined, 'Version should be defined'); - + // Create and connect client client = new SSETestClient(); await client.connect(`http://${HOST}:${port}/sse`); @@ -191,7 +191,7 @@ describe('GitLab MCP Server - SSE Transport', () => { assert.ok(tools !== null && tools !== undefined, 'Tools response should be defined'); assert.ok('tools' in tools, 'Response should have tools property'); assert.ok(Array.isArray(tools.tools) && tools.tools.length > 0, 'Tools array should not be empty'); - + // Check for specific GitLab tools const toolNames = tools.tools.map((tool: any) => tool.name); assert.ok(toolNames.includes('list_merge_requests'), 'Should have list_merge_requests tool'); @@ -202,7 +202,7 @@ describe('GitLab MCP Server - SSE Transport', () => { const result = await client.callTool('list_merge_requests', { project_id: TEST_PROJECT_ID }); - + assert.ok(result !== null && result !== undefined, 'Tool call result should be defined'); assert.ok('content' in result, 'Result should have content property'); }); @@ -233,7 +233,7 @@ describe('GitLab MCP Server - Streamable HTTP Transport', () => { } }); servers.push(server); - + // Verify server started successfully assert.ok(server.process.pid !== undefined, 'Server process should have PID'); assert.strictEqual(server.mode, TransportMode.STREAMABLE_HTTP, 'Server mode should be streamable-http'); @@ -245,7 +245,7 @@ describe('GitLab MCP Server - Streamable HTTP Transport', () => { assert.strictEqual(health.transport, 'streamable-http', 'Transport should be streamable-http'); assert.ok(health.version !== null && health.version !== undefined, 'Version should be defined'); assert.ok(health.activeSessions !== null && health.activeSessions !== undefined, 'Active sessions should be defined'); - + // Create and connect client client = new StreamableHTTPTestClient(); await client.connect(`http://${HOST}:${port}/mcp`); @@ -275,7 +275,7 @@ describe('GitLab MCP Server - Streamable HTTP Transport', () => { assert.ok(tools !== null && tools !== undefined, 'Tools response should be defined'); assert.ok('tools' in tools, 'Response should have tools property'); assert.ok(Array.isArray(tools.tools) && tools.tools.length > 0, 'Tools array should not be empty'); - + // Check for specific GitLab tools const toolNames = tools.tools.map((tool: any) => tool.name); assert.ok(toolNames.includes('list_merge_requests'), 'Should have list_merge_requests tool'); @@ -294,8 +294,8 @@ describe('GitLab MCP Server - Streamable HTTP Transport', () => { const result = await client.callTool('get_project', { project_id: TEST_PROJECT_ID }); - + assert.ok(result !== null && result !== undefined, 'Tool call result should be defined'); assert.ok('content' in result, 'Result should have content property'); }); -}); \ No newline at end of file +}); diff --git a/test/utils/mock-gitlab-server.ts b/test/utils/mock-gitlab-server.ts index 06d9a98d..4c92c02b 100644 --- a/test/utils/mock-gitlab-server.ts +++ b/test/utils/mock-gitlab-server.ts @@ -31,7 +31,7 @@ export class MockGitLabServer { this.config = config; this.app = express(); this.customRouter = express.Router(); - + // Dynamic dispatcher for custom handlers this.customRouter.use((req, res, next) => { // Create a key from method and path (relative to /api/v4) @@ -39,7 +39,7 @@ export class MockGitLabServer { const key = `${req.method.toUpperCase()}:${req.path}`; console.log(`[CustomRouter] Checking key: '${key}'`); const handler = this.customHandlers.get(key); - + if (handler) { console.log(`[MockServer] Custom handler hit: ${key}`); return handler(req, res, next); @@ -349,11 +349,11 @@ export class MockGitLabServer { * Helper to find available port for mock server */ export async function findMockServerPort( - basePort: number = 9000, + basePort: number = 9000, maxAttempts: number = 10 ): Promise { const net = await import('net'); - + const tryPort = async (port: number, attemptsLeft: number): Promise => { if (attemptsLeft === 0) { throw new Error(`Could not find available port after ${maxAttempts} attempts starting from ${basePort}`); @@ -362,7 +362,7 @@ export async function findMockServerPort( return new Promise((resolve, reject) => { const server = net.createServer(); server.unref(); - + server.on('error', async () => { try { const nextPort = await tryPort(port + 1, attemptsLeft - 1); @@ -371,7 +371,7 @@ export async function findMockServerPort( reject(err); } }); - + server.listen(port, '127.0.0.1', () => { const addr = server.address(); const actualPort = typeof addr === 'object' && addr ? addr.port : port; @@ -391,4 +391,3 @@ export async function findMockServerPort( export function resetMockServerState(server: MockGitLabServer) { (server as any).requestCount = 0; } - diff --git a/test/utils/server-launcher.ts b/test/utils/server-launcher.ts index 95cb7949..b8a32a82 100644 --- a/test/utils/server-launcher.ts +++ b/test/utils/server-launcher.ts @@ -45,10 +45,10 @@ export async function launchServer(config: ServerConfig): Promise { if (!serverProcess.killed) { serverProcess.kill('SIGTERM'); - + // Force kill if not terminated within 5 seconds setTimeout(() => { if (!serverProcess.killed) { @@ -145,7 +145,7 @@ async function waitForServerStart( reject(e); return; } - + // Check for server start messages const startMessages = [ 'Starting GitLab MCP Server with stdio transport', @@ -155,7 +155,7 @@ async function waitForServerStart( `port ${port}` ]; - const hasStartMessage = startMessages.some(msg => + const hasStartMessage = startMessages.some(msg => outputBuffer.includes(msg) ); @@ -163,7 +163,7 @@ async function waitForServerStart( clearTimeout(timer); // process.stdout?.removeListener('data', onData); // process.stderr?.removeListener('data', onData); - + // Additional wait for HTTP servers to be fully ready if (mode !== TransportMode.STDIO) { setTimeout(resolve, 1000); @@ -202,17 +202,17 @@ async function waitForServerStart( */ export async function findAvailablePort(basePort: number = 3002): Promise { const net = await import('net'); - + return new Promise((resolve, reject) => { const server = net.createServer(); - + server.listen(basePort, () => { const address = server.address(); const port = typeof address === 'object' && address ? address.port : basePort; - + server.close(() => resolve(port)); }); - + server.on('error', (err: NodeJS.ErrnoException) => { if (err.code === 'EADDRINUSE') { // Port is in use, try next one @@ -235,7 +235,7 @@ export function cleanupServers(servers: ServerInstance[]): void { console.warn(`Failed to kill server process: ${error}`); } }); -} +} /** @@ -262,7 +262,7 @@ export function createTimeoutController(timeout: number): AbortController { */ export async function checkHealthEndpoint(port: number, maxRetries: number = 5): Promise { let lastError: Error; - + for (let i = 0; i < maxRetries; i++) { try { const controller = createTimeoutController(5000); @@ -270,7 +270,7 @@ export async function checkHealthEndpoint(port: number, maxRetries: number = 5): method: 'GET', signal: controller.signal }); - + if (response.ok) { const healthData = await response.json() as HealthCheckResponse; return healthData; @@ -283,13 +283,13 @@ export async function checkHealthEndpoint(port: number, maxRetries: number = 5): } else { lastError = error instanceof Error ? error : new Error(String(error)); } - + if (i < maxRetries - 1) { // Wait before retry await new Promise(resolve => setTimeout(resolve, 1000)); } } } - + throw lastError!; -} \ No newline at end of file +}