Skip to content

Commit ce30fc8

Browse files
authored
Merge pull request #71 from devroopsaha744/mcp
feat(mcp): add modular MCP server & Inspector/Claude integration
2 parents 09b59be + 0ee9641 commit ce30fc8

17 files changed

+1471
-37
lines changed

README.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,62 @@ 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+
### MCP client setup
51+
52+
1. The configuration is the same across operating systems as long as your MCP client is installed and supports external servers (Claude Desktop, Cursor, Windsurf, etc.).
53+
54+
2. Add a server entry pointing at the built file by pasting the JSON below into your MCP client's config file — for example:
55+
56+
- `claude_desktop_config.json` for Claude Desktop
57+
- `mcp.json` for Cursor
58+
- the equivalent JSON config file for other MCP clients
59+
60+
Example (paste into the appropriate file):
61+
62+
```json
63+
{
64+
"mcpServers": {
65+
"leetcode-suite": {
66+
"command": "node",
67+
"args": ["C:\\path\\to\\alfa-leetcode-api\\dist\\mcp\\index.js"]
68+
}
69+
}
70+
}
71+
```
72+
73+
3. Restart your MCP client. A "Search & tools" toggle (or similar UI element) should appear once the server launches successfully.
74+
75+
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.
76+
77+
### MCP Inspector
78+
79+
Use the Inspector to debug tools locally:
80+
81+
```powershell
82+
npx @modelcontextprotocol/inspector node C:\path\to\alfa-leetcode-api\dist\mcp\index.js
83+
```
84+
85+
For TypeScript-on-the-fly development:
86+
87+
```powershell
88+
npx @modelcontextprotocol/inspector npx ts-node mcp/index.ts
89+
```
90+
91+
Choose the *Tools* tab in the Inspector UI to invoke individual operations and confirm responses before wiring them into Claude.
92+
3793
## Wanna Contribute 🤔??
3894

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

mcp/index.ts

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

mcp/leetCodeService.ts

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
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+
import { SubmissionArgs, CalendarArgs, ProblemArgs, DiscussCommentsArgs, Variables } from './types';
47+
48+
// Builds GraphQL variables by filtering out undefined, null, and NaN values.
49+
function buildVariables(input: Record<string, unknown>): Variables {
50+
const result: Variables = {};
51+
for (const [key, value] of Object.entries(input)) {
52+
if (value !== undefined && value !== null && !(typeof value === 'number' && Number.isNaN(value))) {
53+
result[key] = value;
54+
}
55+
}
56+
return result;
57+
}
58+
59+
// Retrieves the formatted user profile summary.
60+
export async function getUserProfileSummary(username: string) {
61+
const data = await executeGraphQL(userProfileQuery, { username });
62+
return formatUserData(data as UserData);
63+
}
64+
65+
// Retrieves the formatted user badges data.
66+
export async function getUserBadges(username: string) {
67+
const data = await executeGraphQL(userProfileQuery, { username });
68+
return formatBadgesData(data as UserData);
69+
}
70+
71+
// Retrieves the formatted user contest data.
72+
export async function getUserContest(username: string) {
73+
const data = await executeGraphQL(contestQuery, { username });
74+
return formatContestData(data as UserData);
75+
}
76+
77+
// Retrieves the formatted user contest history.
78+
export async function getUserContestHistory(username: string) {
79+
const data = await executeGraphQL(contestQuery, { username });
80+
return formatContestHistoryData(data as UserData);
81+
}
82+
83+
// Retrieves the formatted solved problems statistics.
84+
export async function getSolvedProblems(username: string) {
85+
const data = await executeGraphQL(userProfileQuery, { username });
86+
return formatSolvedProblemsData(data as UserData);
87+
}
88+
89+
// Retrieves recent submissions for a user.
90+
export async function getRecentSubmission(args: SubmissionArgs) {
91+
const variables = buildVariables({ username: args.username, limit: args.limit });
92+
const data = await executeGraphQL(submissionQuery, variables);
93+
return formatSubmissionData(data as UserData);
94+
}
95+
96+
// Retrieves recent accepted submissions for a user.
97+
export async function getRecentAcSubmission(args: SubmissionArgs) {
98+
const variables = buildVariables({ username: args.username, limit: args.limit });
99+
const data = await executeGraphQL(AcSubmissionQuery, variables);
100+
return formatAcSubmissionData(data as UserData);
101+
}
102+
103+
// Retrieves the submission calendar for a user in a given year.
104+
export async function getSubmissionCalendar(args: CalendarArgs) {
105+
const variables = buildVariables({ username: args.username, year: args.year });
106+
const data = await executeGraphQL(userProfileCalendarQuery, variables);
107+
return formatSubmissionCalendarData(data as UserData);
108+
}
109+
110+
// Retrieves the aggregated user profile data.
111+
export async function getUserProfileAggregate(username: string) {
112+
const data = await executeGraphQL(getUserProfileQuery, { username });
113+
return formatUserProfileData(data);
114+
}
115+
116+
// Retrieves the language statistics for a user.
117+
export async function getLanguageStats(username: string) {
118+
const data = await executeGraphQL(languageStatsQuery, { username });
119+
return formatLanguageStats(data as UserData);
120+
}
121+
122+
// Retrieves the skill statistics for a user.
123+
export async function getSkillStats(username: string) {
124+
const data = await executeGraphQL(skillStatsQuery, { username });
125+
return formatSkillStats(data as UserData);
126+
}
127+
128+
// Retrieves the daily problem.
129+
export async function getDailyProblem() {
130+
const data = await executeGraphQL(dailyProblemQuery, {});
131+
return formatDailyData(data as DailyProblemData);
132+
}
133+
134+
// Retrieves the raw daily problem data.
135+
export async function getDailyProblemRaw() {
136+
return executeGraphQL(dailyProblemQuery, {});
137+
}
138+
139+
// Retrieves a selected problem by title slug.
140+
export async function getSelectProblem(titleSlug: string) {
141+
const data = await executeGraphQL(selectProblemQuery, { titleSlug });
142+
return formatQuestionData(data as SelectProblemData);
143+
}
144+
145+
// Retrieves the raw data for a selected problem by title slug.
146+
export async function getSelectProblemRaw(titleSlug: string) {
147+
return executeGraphQL(selectProblemQuery, { titleSlug });
148+
}
149+
150+
// Retrieves a list of problems based on the given arguments.
151+
export async function getProblemSet(args: ProblemArgs) {
152+
const limit = args.skip !== undefined && args.limit === undefined ? 1 : args.limit ?? 20;
153+
const skip = args.skip ?? 0;
154+
const tags = args.tags ? args.tags.split(' ') : [];
155+
const difficulty = args.difficulty ?? undefined;
156+
const variables = buildVariables({
157+
categorySlug: '',
158+
limit,
159+
skip,
160+
filters: {
161+
tags,
162+
difficulty,
163+
},
164+
});
165+
const data = await executeGraphQL(problemListQuery, variables);
166+
return formatProblemsData(data as ProblemSetQuestionListData);
167+
}
168+
169+
// Retrieves the official solution for a problem.
170+
export async function getOfficialSolution(titleSlug: string) {
171+
return executeGraphQL(officialSolutionQuery, { titleSlug });
172+
}
173+
174+
// Retrieves trending discussion topics.
175+
export async function getTrendingTopics(first: number) {
176+
const data = await executeGraphQL(trendingDiscussQuery, { first });
177+
return formatTrendingCategoryTopicData(data as TrendingDiscussionObject);
178+
}
179+
180+
// Retrieves a discussion topic by ID.
181+
export async function getDiscussTopic(topicId: number) {
182+
return executeGraphQL(discussTopicQuery, { topicId });
183+
}
184+
185+
// Retrieves comments for a discussion topic.
186+
export async function getDiscussComments(args: DiscussCommentsArgs) {
187+
const variables = buildVariables({
188+
topicId: args.topicId,
189+
orderBy: args.orderBy ?? 'newest_to_oldest',
190+
pageNo: args.pageNo ?? 1,
191+
numPerPage: args.numPerPage ?? 10,
192+
});
193+
return executeGraphQL(discussCommentsQuery, variables);
194+
}
195+
196+
// Retrieves raw language statistics for a user.
197+
export async function getLanguageStatsRaw(username: string) {
198+
return executeGraphQL(languageStatsQuery, { username });
199+
}
200+
201+
// Retrieves raw submission calendar data for a user.
202+
export async function getUserProfileCalendarRaw(args: CalendarArgs) {
203+
const variables = buildVariables({ username: args.username, year: args.year });
204+
return executeGraphQL(userProfileCalendarQuery, variables);
205+
}
206+
207+
// Retrieves raw user profile data.
208+
export async function getUserProfileRaw(username: string) {
209+
return executeGraphQL(getUserProfileQuery, { username });
210+
}
211+
212+
// Retrieves the legacy daily problem data.
213+
export async function getDailyProblemLegacy() {
214+
return executeGraphQL(dailyProblemQuery, {});
215+
}
216+
217+
// Retrieves raw skill statistics for a user.
218+
export async function getSkillStatsRaw(username: string) {
219+
return executeGraphQL(skillStatsQuery, { username });
220+
}
221+
222+
// Retrieves the question progress for a user.
223+
export async function getUserProgress(username: string) {
224+
const data = await executeGraphQL(userQuestionProgressQuery, { username });
225+
return formatProgressStats(data as UserData);
226+
}
227+
228+
// Retrieves the contest ranking information for a user.
229+
export async function getUserContestRankingInfo(username: string) {
230+
return executeGraphQL(userContestRankingInfoQuery, { username });
231+
}
232+
233+
// Retrieves raw question progress data for a user.
234+
export async function getUserProgressRaw(username: string) {
235+
return executeGraphQL(userQuestionProgressQuery, { username });
236+
}

0 commit comments

Comments
 (0)