Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 2 additions & 21 deletions src/common/registry-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
}
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);
Expand Down
5 changes: 5 additions & 0 deletions src/leetcode/leetcode-global-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
9 changes: 9 additions & 0 deletions src/leetcode/leetcode-service-interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
4 changes: 4 additions & 0 deletions src/mcp/tools/auth-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
{
Expand Down
89 changes: 89 additions & 0 deletions src/mcp/tools/onboarding-tools.ts
Original file line number Diff line number Diff line change
@@ -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();
}
4 changes: 2 additions & 2 deletions src/mcp/tools/problem-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/mcp/tools/solution-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 1 addition & 9 deletions src/mcp/tools/user-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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",
{
Expand Down
6 changes: 4 additions & 2 deletions tests/helpers/mock-leetcode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
}
5 changes: 5 additions & 0 deletions tests/helpers/test-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,13 +151,18 @@ 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);

// Register auth prompts
registerAuthPrompts(server, leetcodeService);

// Register onboarding tools
registerOnboardingTools(server, leetcodeService);

// Register all tools
registerProblemTools(server, leetcodeService);
registerUserTools(server, leetcodeService);
Expand Down
52 changes: 51 additions & 1 deletion tests/integration/auth-tools-integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof createMockLeetCodeService>;
Expand Down Expand Up @@ -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
);
});
});
Loading