From b61909673a98d6ae958a8ee7663b1a2bb13e387b Mon Sep 17 00:00:00 2001 From: Svetlana Perekrestova Date: Wed, 4 Feb 2026 23:23:43 +0100 Subject: [PATCH 1/6] feat: add get_started onboarding tool with usage guide Adds a zero-param public get_started tool via OnboardingToolRegistry that returns prompt invocation rules, session start flow, learning mode hint levels, auth flow, and submission language map as live conversation context. Wired into server init (after prompts, before other tools) and test-client helper. Includes 6 integration tests. Co-Authored-By: Claude Sonnet 4.5 --- src/index.ts | 3 + src/mcp/tools/onboarding-tools.ts | 89 +++++++++++++ src/mcp/tools/problem-tools.ts | 4 +- src/mcp/tools/solution-tools.ts | 2 +- tests/helpers/test-client.ts | 5 + .../onboarding-tools-integration.test.ts | 122 ++++++++++++++++++ .../problem-tools-integration.test.ts | 6 + .../solution-tools-integration.test.ts | 1 + 8 files changed, 229 insertions(+), 3 deletions(-) create mode 100644 src/mcp/tools/onboarding-tools.ts create mode 100644 tests/integration/onboarding-tools-integration.test.ts diff --git a/src/index.ts b/src/index.ts index 9b6b0f3..ecc098e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,6 +14,7 @@ import { registerProblemResources } from "./mcp/resources/problem-resources.js"; import { registerSolutionResources } from "./mcp/resources/solution-resources.js"; import { registerAuthTools } from "./mcp/tools/auth-tools.js"; import { registerContestTools } from "./mcp/tools/contest-tools.js"; +import { registerOnboardingTools } from "./mcp/tools/onboarding-tools.js"; import { registerProblemTools } from "./mcp/tools/problem-tools.js"; import { registerSolutionTools } from "./mcp/tools/solution-tools.js"; import { registerSubmissionTools } from "./mcp/tools/submission-tools.js"; @@ -126,6 +127,8 @@ async function main() { // Register MCP prompts for authentication guidance registerAuthPrompts(server, leetcodeService); + registerOnboardingTools(server, leetcodeService); + registerProblemTools(server, leetcodeService); registerUserTools(server, leetcodeService); registerContestTools(server, leetcodeService); diff --git a/src/mcp/tools/onboarding-tools.ts b/src/mcp/tools/onboarding-tools.ts new file mode 100644 index 0000000..66ae70a --- /dev/null +++ b/src/mcp/tools/onboarding-tools.ts @@ -0,0 +1,89 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { LeetcodeServiceInterface } from "../../leetcode/leetcode-service-interface.js"; +import { ToolRegistry } from "./tool-registry.js"; + +const USAGE_GUIDE = `# Interactive LeetCode MCP — Usage Guide + +## Prompts (must be explicitly invoked — not auto-active) + +| Prompt | When to invoke | Params | +|--------|---------------|--------| +| leetcode_learning_mode | START of any practice session, before discussing a problem | none | +| leetcode_problem_workflow | Once the user selects a specific problem | problemSlug, difficulty | +| leetcode_workspace_setup | After problem selection, before user starts coding | language, problemSlug, codeTemplate | +| leetcode_authentication_guide | Whenever auth is needed — first use, 401 errors, or expired credentials | none | + +## Session Start Flow + +1. Invoke leetcode_learning_mode +2. User picks a problem (daily challenge or search) +3. Invoke leetcode_problem_workflow (problemSlug, difficulty) +4. Invoke leetcode_workspace_setup (language, problemSlug, codeTemplate) +5. Guide user with progressive hints (4 levels) +6. Submit with submit_solution when ready + +## Learning Mode Rules + +- Never show a full solution without first working through hint levels 1 → 2 → 3 +- Level 1: Guiding questions ("What pattern do you see?") +- Level 2: General approaches ("Consider a hash map...") +- Level 3: Specific hints ("Iterate once while tracking seen values...") +- Level 4: Pseudocode or partial implementation +- Only show complete solutions when explicitly requested AFTER earlier hints have been delivered +- get_problem_solution and list_problem_solutions return full community solutions — only use at Level 4 or on explicit "show me the solution" after hints + +## Auth Flow + +1. Before any auth-sensitive action, call check_auth_status first +2. If not authenticated or expired → invoke leetcode_authentication_guide prompt (do NOT ad-hoc the instructions) +3. The prompt guides: start_leetcode_auth → user extracts cookies → save_leetcode_credentials +4. On success, retry the original action +5. On any 401 from a tool: call check_auth_status → follow steps 2-4 + +## Submission Language Map + +| User says | Pass this to submit_solution | +|-----------|------------------------------| +| Python / Python 3 | python3 | +| Python 2 | python | +| Java | java | +| C++ | cpp | +| JavaScript | javascript | +| TypeScript | typescript | + +Default: "Python" without version → python3`; + +/** + * Onboarding tool registry. Provides a get_started tool that returns + * usage guidance via its response — prompt invocation rules, session + * flow, learning mode rules, auth flow, and language map. + */ +export class OnboardingToolRegistry extends ToolRegistry { + protected registerPublic(): void { + this.server.registerTool( + "get_started", + { + description: + "Returns the usage guide for this MCP server: which prompts to invoke and when, session flow, auth flow, and submission language map. Call this at the start of any LeetCode practice session." + }, + async () => { + return { + content: [ + { + type: "text", + text: USAGE_GUIDE + } + ] + }; + } + ); + } +} + +export function registerOnboardingTools( + server: McpServer, + leetcodeService: LeetcodeServiceInterface +): void { + const registry = new OnboardingToolRegistry(server, leetcodeService); + registry.register(); +} diff --git a/src/mcp/tools/problem-tools.ts b/src/mcp/tools/problem-tools.ts index ad19e6a..a32f337 100644 --- a/src/mcp/tools/problem-tools.ts +++ b/src/mcp/tools/problem-tools.ts @@ -15,7 +15,7 @@ export class ProblemToolRegistry extends ToolRegistry { "get_daily_challenge", { description: - "Retrieves today's LeetCode Daily Challenge problem with complete details, including problem description, constraints, and examples" + "Retrieves today's LeetCode Daily Challenge problem with complete details, including problem description, constraints, and examples. After fetching, invoke the leetcode_learning_mode and leetcode_problem_workflow prompts before helping the user work on it." }, async () => { const data = await this.leetcodeService.fetchDailyChallenge(); @@ -38,7 +38,7 @@ export class ProblemToolRegistry extends ToolRegistry { "get_problem", { description: - "Retrieves details about a specific LeetCode problem, including its description, examples, constraints, and related information", + "Retrieves details about a specific LeetCode problem, including its description, examples, constraints, and related information. After fetching, invoke the leetcode_learning_mode and leetcode_problem_workflow prompts before helping the user work on it.", inputSchema: { titleSlug: z diff --git a/src/mcp/tools/solution-tools.ts b/src/mcp/tools/solution-tools.ts index 08844bb..ba01ffd 100644 --- a/src/mcp/tools/solution-tools.ts +++ b/src/mcp/tools/solution-tools.ts @@ -112,7 +112,7 @@ export class SolutionToolRegistry extends ToolRegistry { "get_problem_solution", { description: - "Retrieves the complete content and metadata of a specific solution, including the full article text, author information, and related navigation links", + "Retrieves the complete content and metadata of a specific solution, including the full article text, author information, and related navigation links. This returns a FULL community solution — only call this after the user has exhausted progressive hints or has explicitly requested the solution after receiving earlier hints.", inputSchema: { topicId: z diff --git a/tests/helpers/test-client.ts b/tests/helpers/test-client.ts index 2d5f765..76d7d89 100644 --- a/tests/helpers/test-client.ts +++ b/tests/helpers/test-client.ts @@ -151,6 +151,8 @@ export async function createTestClientWithServer( await import("../../src/mcp/prompts/learning-prompts.js"); const { registerAuthPrompts } = await import("../../src/mcp/prompts/auth-prompts.js"); + const { registerOnboardingTools } = + await import("../../src/mcp/tools/onboarding-tools.js"); // Register learning prompts registerLearningPrompts(server, leetcodeService); @@ -158,6 +160,9 @@ export async function createTestClientWithServer( // Register auth prompts registerAuthPrompts(server, leetcodeService); + // Register onboarding tools + registerOnboardingTools(server, leetcodeService); + // Register all tools registerProblemTools(server, leetcodeService); registerUserTools(server, leetcodeService); diff --git a/tests/integration/onboarding-tools-integration.test.ts b/tests/integration/onboarding-tools-integration.test.ts new file mode 100644 index 0000000..a537d53 --- /dev/null +++ b/tests/integration/onboarding-tools-integration.test.ts @@ -0,0 +1,122 @@ +/** + * Onboarding Tools Integration Tests + * Tests the get_started tool through MCP protocol + */ +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { registerOnboardingTools } from "../../src/mcp/tools/onboarding-tools.js"; +import { createMockLeetCodeService } from "../helpers/mock-leetcode.js"; +import type { TestClientPair } from "../helpers/test-client.js"; +import { createTestClient } from "../helpers/test-client.js"; +import { INTEGRATION_TEST_TIMEOUT, assertions } from "./setup.js"; + +describe("Onboarding Tools Integration", () => { + let testClient: TestClientPair; + + beforeEach(async () => { + const mockService = createMockLeetCodeService(); + testClient = await createTestClient({}, (server) => { + registerOnboardingTools(server, mockService as any); + }); + }, INTEGRATION_TEST_TIMEOUT); + + afterEach(async () => { + if (testClient) { + await testClient.cleanup(); + } + }); + + describe("get_started", () => { + it( + "should be registered and description tells Claude to call it first", + async () => { + const { tools } = await testClient.client.listTools(); + const tool = tools.find((t) => t.name === "get_started"); + + expect(tool).toBeDefined(); + expect(tool?.description).toContain("start"); + expect(tool?.description).toContain("prompt"); + }, + INTEGRATION_TEST_TIMEOUT + ); + + it( + "should return prompt invocation rules", + async () => { + const result: any = await testClient.client.callTool({ + name: "get_started", + arguments: {} + }); + + assertions.hasToolResultStructure(result); + const text = result.content[0].text as string; + + expect(text).toContain("leetcode_learning_mode"); + expect(text).toContain("leetcode_problem_workflow"); + expect(text).toContain("leetcode_workspace_setup"); + expect(text).toContain("leetcode_authentication_guide"); + }, + INTEGRATION_TEST_TIMEOUT + ); + + it( + "should return session start flow", + async () => { + const result: any = await testClient.client.callTool({ + name: "get_started", + arguments: {} + }); + + const text = result.content[0].text as string; + expect(text).toContain("Session Start Flow"); + }, + INTEGRATION_TEST_TIMEOUT + ); + + it( + "should return learning mode hint levels", + async () => { + const result: any = await testClient.client.callTool({ + name: "get_started", + arguments: {} + }); + + const text = result.content[0].text as string; + expect(text).toContain("Level 1"); + expect(text).toContain("Level 2"); + expect(text).toContain("Level 3"); + expect(text).toContain("Level 4"); + }, + INTEGRATION_TEST_TIMEOUT + ); + + it( + "should return auth flow with check_auth_status gate", + async () => { + const result: any = await testClient.client.callTool({ + name: "get_started", + arguments: {} + }); + + const text = result.content[0].text as string; + expect(text).toContain("check_auth_status"); + expect(text).toContain("save_leetcode_credentials"); + }, + INTEGRATION_TEST_TIMEOUT + ); + + it( + "should return submission language map with python3 default", + async () => { + const result: any = await testClient.client.callTool({ + name: "get_started", + arguments: {} + }); + + const text = result.content[0].text as string; + expect(text).toContain("python3"); + expect(text).toContain("Language Map"); + }, + INTEGRATION_TEST_TIMEOUT + ); + }); +}); diff --git a/tests/integration/problem-tools-integration.test.ts b/tests/integration/problem-tools-integration.test.ts index 483ed97..c5dbf0a 100644 --- a/tests/integration/problem-tools-integration.test.ts +++ b/tests/integration/problem-tools-integration.test.ts @@ -43,6 +43,9 @@ describe("Problem Tools Integration", () => { expect(dailyChallengeTool?.description).toContain( "Daily Challenge" ); + expect(dailyChallengeTool?.description).toContain( + "leetcode_learning_mode" + ); }, INTEGRATION_TEST_TIMEOUT ); @@ -100,6 +103,9 @@ describe("Problem Tools Integration", () => { const problemTool = tools.find((t) => t.name === "get_problem"); expect(problemTool).toBeDefined(); expect(problemTool?.description).toContain("problem"); + expect(problemTool?.description).toContain( + "leetcode_learning_mode" + ); }, INTEGRATION_TEST_TIMEOUT ); diff --git a/tests/integration/solution-tools-integration.test.ts b/tests/integration/solution-tools-integration.test.ts index d920563..ba5a4bc 100644 --- a/tests/integration/solution-tools-integration.test.ts +++ b/tests/integration/solution-tools-integration.test.ts @@ -102,6 +102,7 @@ describe("Solution Tools Integration", () => { ); expect(tool).toBeDefined(); expect(tool?.description).toContain("solution"); + expect(tool?.description).toContain("hints"); }, INTEGRATION_TEST_TIMEOUT ); From c6cb7050ba521fd987eb124d347c2df7e23fcffa Mon Sep 17 00:00:00 2001 From: Svetlana Perekrestova Date: Thu, 5 Feb 2026 20:57:51 +0100 Subject: [PATCH 2/6] feat: add updateCredentials to service interface and implementation --- src/leetcode/leetcode-global-service.ts | 5 +++++ src/leetcode/leetcode-service-interface.ts | 9 +++++++++ tests/helpers/mock-leetcode.ts | 6 ++++-- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/leetcode/leetcode-global-service.ts b/src/leetcode/leetcode-global-service.ts index 64e577e..9aecc07 100644 --- a/src/leetcode/leetcode-global-service.ts +++ b/src/leetcode/leetcode-global-service.ts @@ -347,4 +347,9 @@ export class LeetCodeGlobalService implements LeetcodeServiceInterface { !!this.credential.session ); } + + updateCredentials(csrf: string, session: string): void { + this.credential.csrf = csrf; + this.credential.session = session; + } } diff --git a/src/leetcode/leetcode-service-interface.ts b/src/leetcode/leetcode-service-interface.ts index 3b6abb0..70f8141 100644 --- a/src/leetcode/leetcode-service-interface.ts +++ b/src/leetcode/leetcode-service-interface.ts @@ -151,6 +151,15 @@ export interface LeetcodeServiceInterface { */ isAuthenticated(): boolean; + /** + * Updates the in-memory credentials so authenticated API calls + * work immediately after save without a server restart. + * + * @param csrf - The CSRF token + * @param session - The session token + */ + updateCredentials(csrf: string, session: string): void; + /** * Retrieves a list of solutions for a specific problem. * diff --git a/tests/helpers/mock-leetcode.ts b/tests/helpers/mock-leetcode.ts index 0d2e7ff..b6e495e 100644 --- a/tests/helpers/mock-leetcode.ts +++ b/tests/helpers/mock-leetcode.ts @@ -158,7 +158,8 @@ export function createMockLeetCodeService(): LeetcodeServiceInterface { }), // Authentication - isAuthenticated: vi.fn().mockReturnValue(false) + isAuthenticated: vi.fn().mockReturnValue(false), + updateCredentials: vi.fn() } as unknown as LeetcodeServiceInterface; } @@ -195,6 +196,7 @@ export function createMockFailingService(): LeetcodeServiceInterface { fetchQuestionSolutionArticles: vi.fn().mockRejectedValue(error), fetchSolutionArticleDetail: vi.fn().mockRejectedValue(error), submitSolution: vi.fn().mockRejectedValue(error), - isAuthenticated: vi.fn().mockReturnValue(false) + isAuthenticated: vi.fn().mockReturnValue(false), + updateCredentials: vi.fn() } as unknown as LeetcodeServiceInterface; } From b330a417dad62e78208d6e44a438e76f79d30974 Mon Sep 17 00:00:00 2001 From: Svetlana Perekrestova Date: Thu, 5 Feb 2026 21:00:06 +0100 Subject: [PATCH 3/6] feat: update in-memory credentials after successful auth save --- src/mcp/tools/auth-tools.ts | 4 ++ .../auth-tools-integration.test.ts | 52 ++++++++++++++++++- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/src/mcp/tools/auth-tools.ts b/src/mcp/tools/auth-tools.ts index 8c52dfd..63b42d5 100644 --- a/src/mcp/tools/auth-tools.ts +++ b/src/mcp/tools/auth-tools.ts @@ -160,6 +160,10 @@ export class AuthToolRegistry extends ToolRegistry { createdAt: new Date().toISOString() }); + // Update in-memory credentials so authenticated + // tools work immediately without server restart + this.leetcodeService.updateCredentials(csrftoken, session); + return { content: [ { diff --git a/tests/integration/auth-tools-integration.test.ts b/tests/integration/auth-tools-integration.test.ts index 8a68fd4..7a6b874 100644 --- a/tests/integration/auth-tools-integration.test.ts +++ b/tests/integration/auth-tools-integration.test.ts @@ -2,13 +2,37 @@ * Auth Tools Integration Tests * Tests all authentication-related tools through MCP protocol */ -import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { registerAuthTools } from "../../src/mcp/tools/auth-tools.js"; import { createMockLeetCodeService } from "../helpers/mock-leetcode.js"; import type { TestClientPair } from "../helpers/test-client.js"; import { createTestClient } from "../helpers/test-client.js"; import { INTEGRATION_TEST_TIMEOUT, assertions } from "./setup.js"; +vi.mock("axios", () => ({ + default: { + post: vi.fn().mockResolvedValue({ + data: { + data: { + userStatus: { + username: "testuser", + isSignedIn: true + } + } + } + }) + }, + isAxiosError: vi.fn().mockReturnValue(false) +})); + +vi.mock("../../src/utils/credentials.js", () => ({ + credentialsStorage: { + save: vi.fn().mockResolvedValue(undefined), + load: vi.fn().mockResolvedValue(null), + exists: vi.fn().mockResolvedValue(false) + } +})); + describe("Auth Tools Integration", () => { let testClient: TestClientPair; let mockService: ReturnType; @@ -132,4 +156,30 @@ describe("Auth Tools Integration", () => { INTEGRATION_TEST_TIMEOUT ); }); + + describe("save_leetcode_credentials updates in-memory credentials", () => { + it( + "should call updateCredentials on the service after successful save", + async () => { + const result: any = await testClient.client.callTool({ + name: "save_leetcode_credentials", + arguments: { + csrftoken: "test-csrf-token", + session: "test-session-token" + } + }); + + assertions.hasToolResultStructure(result); + const data = JSON.parse(result.content[0].text as string); + + expect(data.status).toBe("success"); + expect(data.username).toBe("testuser"); + expect(mockService.updateCredentials).toHaveBeenCalledWith( + "test-csrf-token", + "test-session-token" + ); + }, + INTEGRATION_TEST_TIMEOUT + ); + }); }); From fd3e343529f3f571ad402724f05c25e612c5d537 Mon Sep 17 00:00:00 2001 From: Svetlana Perekrestova Date: Thu, 5 Feb 2026 21:08:50 +0100 Subject: [PATCH 4/6] test: add failing tests for 4 authenticated user tools (RED phase) --- .../user-tools-integration.test.ts | 268 +++++++++++++++++- 1 file changed, 267 insertions(+), 1 deletion(-) diff --git a/tests/integration/user-tools-integration.test.ts b/tests/integration/user-tools-integration.test.ts index 7f34d22..aeff847 100644 --- a/tests/integration/user-tools-integration.test.ts +++ b/tests/integration/user-tools-integration.test.ts @@ -2,7 +2,7 @@ * User Tools Integration Tests * Tests all user-related tools through MCP protocol */ -import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { registerUserTools } from "../../src/mcp/tools/user-tools.js"; import { createMockLeetCodeService } from "../helpers/mock-leetcode.js"; import type { TestClientPair } from "../helpers/test-client.js"; @@ -123,4 +123,270 @@ describe("User Tools Integration", () => { INTEGRATION_TEST_TIMEOUT ); }); + + describe("get_user_status", () => { + it( + "should list get_user_status tool", + async () => { + const { tools } = await testClient.client.listTools(); + const tool = tools.find((t) => t.name === "get_user_status"); + + expect(tool).toBeDefined(); + expect(tool?.description).toContain("status"); + }, + INTEGRATION_TEST_TIMEOUT + ); + + it( + "should execute get_user_status successfully", + async () => { + const result: any = await testClient.client.callTool({ + name: "get_user_status", + arguments: {} + }); + + assertions.hasToolResultStructure(result); + const data = JSON.parse(result.content[0].text as string); + expect(data.status).toBeDefined(); + expect(mockService.fetchUserStatus).toHaveBeenCalledOnce(); + }, + INTEGRATION_TEST_TIMEOUT + ); + + it( + "should return error when service throws auth error", + async () => { + const failingService = createMockLeetCodeService(); + vi.mocked(failingService.fetchUserStatus).mockRejectedValue( + new Error("Authentication required to fetch user status") + ); + const failingClient = await createTestClient({}, (server) => { + registerUserTools(server, failingService as any); + }); + + try { + const result: any = await failingClient.client.callTool({ + name: "get_user_status", + arguments: {} + }); + + assertions.hasToolResultStructure(result); + const data = JSON.parse(result.content[0].text as string); + expect(data.error).toContain("Authentication required"); + } finally { + await failingClient.cleanup(); + } + }, + INTEGRATION_TEST_TIMEOUT + ); + }); + + describe("get_problem_submission_report", () => { + it( + "should list get_problem_submission_report tool", + async () => { + const { tools } = await testClient.client.listTools(); + const tool = tools.find( + (t) => t.name === "get_problem_submission_report" + ); + + expect(tool).toBeDefined(); + expect(tool?.description).toContain("submission"); + }, + INTEGRATION_TEST_TIMEOUT + ); + + it( + "should execute get_problem_submission_report successfully", + async () => { + const result: any = await testClient.client.callTool({ + name: "get_problem_submission_report", + arguments: { id: 12345 } + }); + + assertions.hasToolResultStructure(result); + const data = JSON.parse(result.content[0].text as string); + expect(data.submissionId).toBe(12345); + expect( + mockService.fetchUserSubmissionDetail + ).toHaveBeenCalledWith(12345); + }, + INTEGRATION_TEST_TIMEOUT + ); + + it( + "should return error when service throws auth error", + async () => { + const failingService = createMockLeetCodeService(); + vi.mocked( + failingService.fetchUserSubmissionDetail + ).mockRejectedValue( + new Error( + "Authentication required to fetch user submission detail" + ) + ); + const failingClient = await createTestClient({}, (server) => { + registerUserTools(server, failingService as any); + }); + + try { + const result: any = await failingClient.client.callTool({ + name: "get_problem_submission_report", + arguments: { id: 12345 } + }); + + assertions.hasToolResultStructure(result); + const data = JSON.parse(result.content[0].text as string); + expect(data.error).toContain("Authentication required"); + } finally { + await failingClient.cleanup(); + } + }, + INTEGRATION_TEST_TIMEOUT + ); + }); + + describe("get_problem_progress", () => { + it( + "should list get_problem_progress tool", + async () => { + const { tools } = await testClient.client.listTools(); + const tool = tools.find( + (t) => t.name === "get_problem_progress" + ); + + expect(tool).toBeDefined(); + expect(tool?.description).toContain("progress"); + }, + INTEGRATION_TEST_TIMEOUT + ); + + it( + "should execute get_problem_progress successfully", + async () => { + const result: any = await testClient.client.callTool({ + name: "get_problem_progress", + arguments: { + offset: 0, + limit: 10, + questionStatus: "SOLVED", + difficulty: ["EASY"] + } + }); + + assertions.hasToolResultStructure(result); + expect( + mockService.fetchUserProgressQuestionList + ).toHaveBeenCalledWith({ + offset: 0, + limit: 10, + questionStatus: "SOLVED", + difficulty: ["EASY"] + }); + }, + INTEGRATION_TEST_TIMEOUT + ); + + it( + "should return error when service throws auth error", + async () => { + const failingService = createMockLeetCodeService(); + vi.mocked( + failingService.fetchUserProgressQuestionList + ).mockRejectedValue( + new Error( + "Authentication required to fetch user progress question list" + ) + ); + const failingClient = await createTestClient({}, (server) => { + registerUserTools(server, failingService as any); + }); + + try { + const result: any = await failingClient.client.callTool({ + name: "get_problem_progress", + arguments: { offset: 0, limit: 10 } + }); + + assertions.hasToolResultStructure(result); + const data = JSON.parse(result.content[0].text as string); + expect(data.error).toContain("Authentication required"); + } finally { + await failingClient.cleanup(); + } + }, + INTEGRATION_TEST_TIMEOUT + ); + }); + + describe("get_all_submissions", () => { + it( + "should list get_all_submissions tool", + async () => { + const { tools } = await testClient.client.listTools(); + const tool = tools.find( + (t) => t.name === "get_all_submissions" + ); + + expect(tool).toBeDefined(); + expect(tool?.description).toContain("submissions"); + }, + INTEGRATION_TEST_TIMEOUT + ); + + it( + "should execute get_all_submissions successfully", + async () => { + const result: any = await testClient.client.callTool({ + name: "get_all_submissions", + arguments: { + limit: 10, + offset: 0, + questionSlug: "two-sum" + } + }); + + assertions.hasToolResultStructure(result); + expect( + mockService.fetchUserAllSubmissions + ).toHaveBeenCalledWith({ + offset: 0, + limit: 10, + questionSlug: "two-sum" + }); + }, + INTEGRATION_TEST_TIMEOUT + ); + + it( + "should return error when service throws auth error", + async () => { + const failingService = createMockLeetCodeService(); + vi.mocked( + failingService.fetchUserAllSubmissions + ).mockRejectedValue( + new Error( + "Authentication required to fetch user submissions" + ) + ); + const failingClient = await createTestClient({}, (server) => { + registerUserTools(server, failingService as any); + }); + + try { + const result: any = await failingClient.client.callTool({ + name: "get_all_submissions", + arguments: { limit: 10, offset: 0 } + }); + + assertions.hasToolResultStructure(result); + const data = JSON.parse(result.content[0].text as string); + expect(data.error).toContain("Authentication required"); + } finally { + await failingClient.cleanup(); + } + }, + INTEGRATION_TEST_TIMEOUT + ); + }); }); From 275d6336a19acdb0d5877a81d8692423d44ae5ac Mon Sep 17 00:00:00 2001 From: Svetlana Perekrestova Date: Thu, 5 Feb 2026 21:11:29 +0100 Subject: [PATCH 5/6] refactor: remove two-phase registration, move all tools to registerPublic --- src/common/registry-base.ts | 23 ++----------------- src/mcp/tools/user-tools.ts | 10 +------- .../user-tools-integration.test.ts | 10 +++++--- 3 files changed, 10 insertions(+), 33 deletions(-) diff --git a/src/common/registry-base.ts b/src/common/registry-base.ts index ffbc622..4f7fe95 100644 --- a/src/common/registry-base.ts +++ b/src/common/registry-base.ts @@ -19,33 +19,14 @@ export abstract class RegistryBase { ) {} /** - * Determines if the current LeetCode service has valid authentication credentials. - * - * @returns True if authenticated, false otherwise - */ - get isAuthenticated(): boolean { - return this.leetcodeService.isAuthenticated(); - } - - /** - * Registers all applicable components based on authentication status. + * Registers all public components. */ public register(): void { this.registerPublic(); - if (this.isAuthenticated) { - this.registerAuthenticated(); - } } /** - * Hook for registering components that don't require authentication. - * Override this in subclasses. + * Hook for registering components. Override in subclasses. */ protected registerPublic(): void {} - - /** - * Hook for registering components that require authentication. - * Override this in subclasses. - */ - protected registerAuthenticated(): void {} } diff --git a/src/mcp/tools/user-tools.ts b/src/mcp/tools/user-tools.ts index 19fb8ca..645c43d 100644 --- a/src/mcp/tools/user-tools.ts +++ b/src/mcp/tools/user-tools.ts @@ -8,10 +8,6 @@ import { ToolRegistry } from "./tool-registry.js"; * This class manages tools for accessing user profiles, submissions, and progress data. */ export class UserToolRegistry extends ToolRegistry { - protected get requiresAuthentication(): boolean { - return true; - } - protected registerPublic(): void { // User profile tool this.server.registerTool( @@ -154,12 +150,8 @@ export class UserToolRegistry extends ToolRegistry { } } ); - } - /** - * Registers tools specific to the Global LeetCode site that require authentication. - */ - protected registerAuthenticated(): void { + // User status tool this.server.registerTool( "get_user_status", { diff --git a/tests/integration/user-tools-integration.test.ts b/tests/integration/user-tools-integration.test.ts index aeff847..db0de61 100644 --- a/tests/integration/user-tools-integration.test.ts +++ b/tests/integration/user-tools-integration.test.ts @@ -256,7 +256,7 @@ describe("User Tools Integration", () => { ); expect(tool).toBeDefined(); - expect(tool?.description).toContain("progress"); + expect(tool?.description).toContain("problem-solving"); }, INTEGRATION_TEST_TIMEOUT ); @@ -310,7 +310,9 @@ describe("User Tools Integration", () => { assertions.hasToolResultStructure(result); const data = JSON.parse(result.content[0].text as string); - expect(data.error).toContain("Authentication required"); + expect(data.error).toContain( + "Failed to fetch user progress questions" + ); } finally { await failingClient.cleanup(); } @@ -381,7 +383,9 @@ describe("User Tools Integration", () => { assertions.hasToolResultStructure(result); const data = JSON.parse(result.content[0].text as string); - expect(data.error).toContain("Authentication required"); + expect(data.error).toContain( + "Failed to fetch user submissions" + ); } finally { await failingClient.cleanup(); } From 6996660c53216f3a135de2f9be414758f17ffd92 Mon Sep 17 00:00:00 2001 From: Svetlana Perekrestova Date: Thu, 5 Feb 2026 21:12:28 +0100 Subject: [PATCH 6/6] test: add updateCredentials mock to auth-tools unit test --- tests/mcp/tools/auth-tools.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/mcp/tools/auth-tools.test.ts b/tests/mcp/tools/auth-tools.test.ts index 1fc6903..9df2144 100644 --- a/tests/mcp/tools/auth-tools.test.ts +++ b/tests/mcp/tools/auth-tools.test.ts @@ -47,7 +47,8 @@ describe("AuthToolRegistry", () => { } as unknown as McpServer; mockLeetCodeService = { - isAuthenticated: vi.fn().mockReturnValue(false) + isAuthenticated: vi.fn().mockReturnValue(false), + updateCredentials: vi.fn() } as unknown as LeetcodeServiceInterface; registry = new AuthToolRegistry(mockServer, mockLeetCodeService);