Skip to content

Commit 0778b4d

Browse files
feat(mcp): add modular MCP server & Inspector/Claude integration
1 parent bd3b316 commit 0778b4d

16 files changed

+1374
-42
lines changed

README.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,55 @@ https://alfa-leetcode-api.onrender.com/
3434
docker run -p 3000:3000 alfaarghya/alfa-leetcode-api:2.0.2
3535
```
3636

37+
## MCP server integration 🤖
38+
39+
The repository also ships a Model Context Protocol (MCP) server that exposes the same LeetCode data as interactive tools for Claude Desktop or the MCP Inspector.
40+
41+
### Build
42+
43+
```powershell
44+
npm install
45+
npm run build
46+
```
47+
48+
The build step produces `dist/mcp/index.js`, the entry point used by MCP clients.
49+
50+
### Claude Desktop setup
51+
52+
1. Open `%AppData%/Claude/claude_desktop_config.json`.
53+
2. Add a server entry pointing at the built file. Example:
54+
55+
```json
56+
{
57+
"mcpServers": {
58+
"leetcode-suite": {
59+
"command": "node",
60+
"args": ["C:\\path\\to\\alfa-leetcode-api\\dist\\mcp\\index.js"]
61+
}
62+
}
63+
}
64+
```
65+
66+
3. Restart Claude Desktop. A "Search & tools" toggle appears once the server launches successfully.
67+
68+
To run only a subset of tools, append the module name (`users`, `problems`, or `discussions`) as an extra argument or set the `MCP_SERVER_MODE` environment variable.
69+
70+
### MCP Inspector
71+
72+
Use the Inspector to debug tools locally:
73+
74+
```powershell
75+
npx @modelcontextprotocol/inspector node C:\path\to\alfa-leetcode-api\dist\mcp\index.js
76+
```
77+
78+
For TypeScript-on-the-fly development:
79+
80+
```powershell
81+
npx @modelcontextprotocol/inspector npx ts-node mcp/index.ts
82+
```
83+
84+
Choose the *Tools* tab in the Inspector UI to invoke individual operations and confirm responses before wiring them into Claude.
85+
3786
## Wanna Contribute 🤔??
3887

3988
follow this documentation => <a href="CONTRIBUTING.md" target="_blank">CONTRIBUTING.md</a>

mcp/index.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { DiscussionToolsModule } from './modules/discussionTools';
2+
import { ProblemToolsModule } from './modules/problemTools';
3+
import { UserToolsModule } from './modules/userTools';
4+
import { SERVER_VERSION, startServer, ToolModule } from './serverUtils';
5+
6+
type Mode = 'all' | 'users' | 'problems' | 'discussions';
7+
8+
type ModuleConfig = {
9+
modules: ToolModule[];
10+
name: string;
11+
};
12+
13+
const modulesByKey: Record<Exclude<Mode, 'all'>, ToolModule> = {
14+
users: new UserToolsModule(),
15+
problems: new ProblemToolsModule(),
16+
discussions: new DiscussionToolsModule(),
17+
};
18+
19+
function normalizeMode(input: string | undefined): Mode {
20+
if (!input) {
21+
return 'all';
22+
}
23+
24+
const normalized = input.trim().toLowerCase();
25+
26+
if (normalized === 'all' || normalized === 'suite' || normalized === 'default') {
27+
return 'all';
28+
}
29+
30+
if (normalized in modulesByKey) {
31+
return normalized as Exclude<Mode, 'all'>;
32+
}
33+
34+
console.error(`Unknown MCP server mode: ${input}`);
35+
console.error('Expected one of: all, users, problems, discussions');
36+
process.exit(1);
37+
}
38+
39+
function resolveModules(mode: Mode): ModuleConfig {
40+
if (mode === 'all') {
41+
return {
42+
modules: Object.values(modulesByKey),
43+
name: 'alfa-leetcode-suite',
44+
};
45+
}
46+
47+
return {
48+
modules: [modulesByKey[mode]],
49+
name: `alfa-leetcode-${mode}`,
50+
};
51+
}
52+
53+
async function main() {
54+
const modeInput = process.env.MCP_SERVER_MODE ?? process.argv[2];
55+
const mode = normalizeMode(modeInput);
56+
const { modules, name } = resolveModules(mode);
57+
58+
await startServer({ name, version: SERVER_VERSION }, modules);
59+
}
60+
61+
main().catch((error) => {
62+
console.error(error);
63+
process.exit(1);
64+
});

mcp/leetCodeService.ts

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
import {
2+
formatAcSubmissionData,
3+
formatBadgesData,
4+
formatContestData,
5+
formatContestHistoryData,
6+
formatDailyData,
7+
formatLanguageStats,
8+
formatProblemsData,
9+
formatProgressStats,
10+
formatQuestionData,
11+
formatSolvedProblemsData,
12+
formatSubmissionCalendarData,
13+
formatSubmissionData,
14+
formatTrendingCategoryTopicData,
15+
formatSkillStats,
16+
formatUserData,
17+
formatUserProfileData,
18+
} from '../src/FormatUtils';
19+
import {
20+
AcSubmissionQuery,
21+
contestQuery,
22+
dailyProblemQuery,
23+
discussCommentsQuery,
24+
discussTopicQuery,
25+
getUserProfileQuery,
26+
languageStatsQuery,
27+
officialSolutionQuery,
28+
problemListQuery,
29+
selectProblemQuery,
30+
submissionQuery,
31+
trendingDiscussQuery,
32+
userProfileCalendarQuery,
33+
userProfileQuery,
34+
userQuestionProgressQuery,
35+
userContestRankingInfoQuery,
36+
skillStatsQuery,
37+
} from '../src/GQLQueries';
38+
import type {
39+
DailyProblemData,
40+
ProblemSetQuestionListData,
41+
SelectProblemData,
42+
TrendingDiscussionObject,
43+
UserData,
44+
} from '../src/types';
45+
import { executeGraphQL } from './serverUtils';
46+
47+
type SubmissionArgs = { username: string; limit?: number };
48+
type CalendarArgs = { username: string; year: number };
49+
type ProblemArgs = { limit?: number; skip?: number; tags?: string; difficulty?: string };
50+
type DiscussCommentsArgs = { topicId: number; orderBy?: string; pageNo?: number; numPerPage?: number };
51+
52+
type Variables = Record<string, unknown>;
53+
54+
function buildVariables(input: Record<string, unknown>): Variables {
55+
const result: Variables = {};
56+
for (const [key, value] of Object.entries(input)) {
57+
if (value !== undefined && value !== null && !(typeof value === 'number' && Number.isNaN(value))) {
58+
result[key] = value;
59+
}
60+
}
61+
return result;
62+
}
63+
64+
export async function getUserProfileSummary(username: string) {
65+
const data = await executeGraphQL(userProfileQuery, { username });
66+
return formatUserData(data as UserData);
67+
}
68+
69+
export async function getUserBadges(username: string) {
70+
const data = await executeGraphQL(userProfileQuery, { username });
71+
return formatBadgesData(data as UserData);
72+
}
73+
74+
export async function getUserContest(username: string) {
75+
const data = await executeGraphQL(contestQuery, { username });
76+
return formatContestData(data as UserData);
77+
}
78+
79+
export async function getUserContestHistory(username: string) {
80+
const data = await executeGraphQL(contestQuery, { username });
81+
return formatContestHistoryData(data as UserData);
82+
}
83+
84+
export async function getSolvedProblems(username: string) {
85+
const data = await executeGraphQL(userProfileQuery, { username });
86+
return formatSolvedProblemsData(data as UserData);
87+
}
88+
89+
export async function getRecentSubmission(args: SubmissionArgs) {
90+
const variables = buildVariables({ username: args.username, limit: args.limit });
91+
const data = await executeGraphQL(submissionQuery, variables);
92+
return formatSubmissionData(data as UserData);
93+
}
94+
95+
export async function getRecentAcSubmission(args: SubmissionArgs) {
96+
const variables = buildVariables({ username: args.username, limit: args.limit });
97+
const data = await executeGraphQL(AcSubmissionQuery, variables);
98+
return formatAcSubmissionData(data as UserData);
99+
}
100+
101+
export async function getSubmissionCalendar(args: CalendarArgs) {
102+
const variables = buildVariables({ username: args.username, year: args.year });
103+
const data = await executeGraphQL(userProfileCalendarQuery, variables);
104+
return formatSubmissionCalendarData(data as UserData);
105+
}
106+
107+
export async function getUserProfileAggregate(username: string) {
108+
const data = await executeGraphQL(getUserProfileQuery, { username });
109+
return formatUserProfileData(data);
110+
}
111+
112+
export async function getLanguageStats(username: string) {
113+
const data = await executeGraphQL(languageStatsQuery, { username });
114+
return formatLanguageStats(data as UserData);
115+
}
116+
117+
export async function getSkillStats(username: string) {
118+
const data = await executeGraphQL(skillStatsQuery, { username });
119+
return formatSkillStats(data as UserData);
120+
}
121+
122+
export async function getDailyProblem() {
123+
const data = await executeGraphQL(dailyProblemQuery, {});
124+
return formatDailyData(data as DailyProblemData);
125+
}
126+
127+
export async function getDailyProblemRaw() {
128+
return executeGraphQL(dailyProblemQuery, {});
129+
}
130+
131+
export async function getSelectProblem(titleSlug: string) {
132+
const data = await executeGraphQL(selectProblemQuery, { titleSlug });
133+
return formatQuestionData(data as SelectProblemData);
134+
}
135+
136+
export async function getSelectProblemRaw(titleSlug: string) {
137+
return executeGraphQL(selectProblemQuery, { titleSlug });
138+
}
139+
140+
export async function getProblemSet(args: ProblemArgs) {
141+
const limit = args.skip !== undefined && args.limit === undefined ? 1 : args.limit ?? 20;
142+
const skip = args.skip ?? 0;
143+
const tags = args.tags ? args.tags.split(' ') : [];
144+
const difficulty = args.difficulty ?? undefined;
145+
const variables = buildVariables({
146+
categorySlug: '',
147+
limit,
148+
skip,
149+
filters: {
150+
tags,
151+
difficulty,
152+
},
153+
});
154+
const data = await executeGraphQL(problemListQuery, variables);
155+
return formatProblemsData(data as ProblemSetQuestionListData);
156+
}
157+
158+
export async function getOfficialSolution(titleSlug: string) {
159+
return executeGraphQL(officialSolutionQuery, { titleSlug });
160+
}
161+
162+
export async function getTrendingTopics(first: number) {
163+
const data = await executeGraphQL(trendingDiscussQuery, { first });
164+
return formatTrendingCategoryTopicData(data as TrendingDiscussionObject);
165+
}
166+
167+
export async function getDiscussTopic(topicId: number) {
168+
return executeGraphQL(discussTopicQuery, { topicId });
169+
}
170+
171+
export async function getDiscussComments(args: DiscussCommentsArgs) {
172+
const variables = buildVariables({
173+
topicId: args.topicId,
174+
orderBy: args.orderBy ?? 'newest_to_oldest',
175+
pageNo: args.pageNo ?? 1,
176+
numPerPage: args.numPerPage ?? 10,
177+
});
178+
return executeGraphQL(discussCommentsQuery, variables);
179+
}
180+
181+
export async function getLanguageStatsRaw(username: string) {
182+
return executeGraphQL(languageStatsQuery, { username });
183+
}
184+
185+
export async function getUserProfileCalendarRaw(args: CalendarArgs) {
186+
const variables = buildVariables({ username: args.username, year: args.year });
187+
return executeGraphQL(userProfileCalendarQuery, variables);
188+
}
189+
190+
export async function getUserProfileRaw(username: string) {
191+
return executeGraphQL(getUserProfileQuery, { username });
192+
}
193+
194+
export async function getDailyProblemLegacy() {
195+
return executeGraphQL(dailyProblemQuery, {});
196+
}
197+
198+
export async function getSkillStatsRaw(username: string) {
199+
return executeGraphQL(skillStatsQuery, { username });
200+
}
201+
202+
export async function getUserProgress(username: string) {
203+
const data = await executeGraphQL(userQuestionProgressQuery, { username });
204+
return formatProgressStats(data as UserData);
205+
}
206+
207+
export async function getUserContestRankingInfo(username: string) {
208+
return executeGraphQL(userContestRankingInfoQuery, { username });
209+
}
210+
211+
export async function getUserProgressRaw(username: string) {
212+
return executeGraphQL(userQuestionProgressQuery, { username });
213+
}

mcp/modules/discussionTools.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2+
import { z } from 'zod';
3+
import { getDiscussComments, getDiscussTopic, getTrendingTopics } from '../leetCodeService';
4+
import { runTool, ToolModule } from '../serverUtils';
5+
6+
export class DiscussionToolsModule implements ToolModule {
7+
register(server: McpServer): void {
8+
server.registerTool(
9+
'leetcode_discuss_trending',
10+
{
11+
title: 'Trending Discussions',
12+
description: 'Lists trending discussion topics',
13+
inputSchema: {
14+
first: z.number().int().positive().max(50).optional(),
15+
},
16+
},
17+
async ({ first }) => runTool(() => getTrendingTopics(first ?? 20)),
18+
);
19+
20+
server.registerTool(
21+
'leetcode_discuss_topic',
22+
{
23+
title: 'Discussion Topic',
24+
description: 'Retrieves full topic details by topic id',
25+
inputSchema: {
26+
topicId: z.number().int().positive(),
27+
},
28+
},
29+
async ({ topicId }) => runTool(() => getDiscussTopic(topicId)),
30+
);
31+
32+
server.registerTool(
33+
'leetcode_discuss_comments',
34+
{
35+
title: 'Discussion Comments',
36+
description: 'Retrieves comments for a discussion topic',
37+
inputSchema: {
38+
topicId: z.number().int().positive(),
39+
orderBy: z.string().optional(),
40+
pageNo: z.number().int().positive().optional(),
41+
numPerPage: z.number().int().positive().max(50).optional(),
42+
},
43+
},
44+
async ({ topicId, orderBy, pageNo, numPerPage }) =>
45+
runTool(() => getDiscussComments({ topicId, orderBy, pageNo, numPerPage })),
46+
);
47+
}
48+
}

0 commit comments

Comments
 (0)