stackprep-mcp 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- stackprep_mcp/__init__.py +0 -0
- stackprep_mcp/server.py +389 -0
- stackprep_mcp/skills/certification.md +110 -0
- stackprep_mcp/skills/interview.md +109 -0
- stackprep_mcp-0.1.0.dist-info/METADATA +241 -0
- stackprep_mcp-0.1.0.dist-info/RECORD +8 -0
- stackprep_mcp-0.1.0.dist-info/WHEEL +4 -0
- stackprep_mcp-0.1.0.dist-info/entry_points.txt +2 -0
|
File without changes
|
stackprep_mcp/server.py
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import secrets
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from mcp.server.fastmcp import FastMCP
|
|
11
|
+
|
|
12
|
+
mcp = FastMCP("stackprep")
|
|
13
|
+
|
|
14
|
+
SKILLS_DIR = Path(__file__).parent / "skills"
|
|
15
|
+
|
|
16
|
+
_sessions: dict[str, dict[str, Any]] = {}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ── Storage ────────────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
def _data_dir() -> Path:
|
|
22
|
+
env = os.environ.get("STACKPREP_PACKS_DIR")
|
|
23
|
+
d = Path(env) if env else Path.home() / ".stackprep"
|
|
24
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
25
|
+
return d
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _packs_dir() -> Path:
|
|
29
|
+
d = _data_dir() / "packs"
|
|
30
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
31
|
+
return d
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _sessions_dir() -> Path:
|
|
35
|
+
d = _data_dir() / "sessions"
|
|
36
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
37
|
+
return d
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _persist_session(session_id: str) -> None:
|
|
41
|
+
path = _sessions_dir() / f"{session_id}.json"
|
|
42
|
+
path.write_text(json.dumps(_sessions[session_id], indent=2, ensure_ascii=False), encoding="utf-8")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _restore_session(session_id: str) -> dict | None:
|
|
46
|
+
path = _sessions_dir() / f"{session_id}.json"
|
|
47
|
+
if path.exists():
|
|
48
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
49
|
+
_sessions[session_id] = data
|
|
50
|
+
return data
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _delete_session(session_id: str) -> None:
|
|
55
|
+
path = _sessions_dir() / f"{session_id}.json"
|
|
56
|
+
if path.exists():
|
|
57
|
+
path.unlink()
|
|
58
|
+
_sessions.pop(session_id, None)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ── Skill loading ──────────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
def _load_skill(mode: str) -> str:
|
|
64
|
+
skill_file = SKILLS_DIR / f"{mode}.md"
|
|
65
|
+
if skill_file.exists():
|
|
66
|
+
return skill_file.read_text(encoding="utf-8")
|
|
67
|
+
return ""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ── Tools ──────────────────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
@mcp.tool()
|
|
73
|
+
def start_session(
|
|
74
|
+
mode: str,
|
|
75
|
+
cert_name: str = "",
|
|
76
|
+
cv: str = "",
|
|
77
|
+
jd: str = "",
|
|
78
|
+
extra_topics: str = "",
|
|
79
|
+
) -> str:
|
|
80
|
+
"""Start a new stackprep session. Returns a session ID and the skill rules for the AI to follow.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
mode: "interview" or "certification"
|
|
84
|
+
cert_name: For certification mode — the exam name (e.g. "AWS SAA-C03")
|
|
85
|
+
cv: For interview mode — the user's CV/resume text
|
|
86
|
+
jd: For interview mode — the job description text
|
|
87
|
+
extra_topics: Optional comma-separated extra topics to focus on
|
|
88
|
+
"""
|
|
89
|
+
if mode not in ("interview", "certification"):
|
|
90
|
+
return "ERROR: mode must be 'interview' or 'certification'"
|
|
91
|
+
if mode == "certification" and not cert_name:
|
|
92
|
+
return "ERROR: cert_name is required for certification mode"
|
|
93
|
+
if mode == "interview" and not (cv and jd):
|
|
94
|
+
return "ERROR: cv and jd are required for interview mode"
|
|
95
|
+
|
|
96
|
+
session_id = secrets.token_hex(6)
|
|
97
|
+
|
|
98
|
+
_sessions[session_id] = {
|
|
99
|
+
"mode": mode,
|
|
100
|
+
"cert_name": cert_name,
|
|
101
|
+
"extra_topics": extra_topics,
|
|
102
|
+
"q_num": 0,
|
|
103
|
+
"score": {"correct": 0, "partial": 0, "incorrect": 0, "total": 0},
|
|
104
|
+
"auto_flagged": [],
|
|
105
|
+
"flagged": [],
|
|
106
|
+
"ended": False,
|
|
107
|
+
"all_flagged": [],
|
|
108
|
+
}
|
|
109
|
+
_persist_session(session_id)
|
|
110
|
+
|
|
111
|
+
skill = _load_skill(mode)
|
|
112
|
+
context_lines = [f"Session ID: {session_id}", f"Mode: {mode}"]
|
|
113
|
+
if cert_name:
|
|
114
|
+
context_lines.append(f"Certification: {cert_name}")
|
|
115
|
+
if extra_topics:
|
|
116
|
+
context_lines.append(f"Extra topics: {extra_topics}")
|
|
117
|
+
if cv:
|
|
118
|
+
context_lines.append(f"\n--- CV ---\n{cv}")
|
|
119
|
+
if jd:
|
|
120
|
+
context_lines.append(f"\n--- Job description ---\n{jd}")
|
|
121
|
+
|
|
122
|
+
return "\n".join([
|
|
123
|
+
"=== STACKPREP SESSION STARTED ===",
|
|
124
|
+
"\n".join(context_lines),
|
|
125
|
+
"\n=== SKILL RULES (follow these exactly) ===",
|
|
126
|
+
skill,
|
|
127
|
+
])
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@mcp.tool()
|
|
131
|
+
def submit_answer(session_id: str, result: str, question: str = "") -> str:
|
|
132
|
+
"""Record the result of an answered question.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
session_id: The session ID
|
|
136
|
+
result: "correct", "partial", or "incorrect"
|
|
137
|
+
question: The question text (used to build the study pack)
|
|
138
|
+
"""
|
|
139
|
+
session = _sessions.get(session_id) or _restore_session(session_id)
|
|
140
|
+
if not session:
|
|
141
|
+
return f"ERROR: No session found with ID '{session_id}'."
|
|
142
|
+
|
|
143
|
+
result = result.lower().strip()
|
|
144
|
+
if result not in ("correct", "partial", "incorrect"):
|
|
145
|
+
return "ERROR: result must be 'correct', 'partial', or 'incorrect'"
|
|
146
|
+
|
|
147
|
+
session["q_num"] += 1
|
|
148
|
+
session["score"]["total"] += 1
|
|
149
|
+
q_num = session["q_num"]
|
|
150
|
+
snippet = question[:200] if question else f"Q{q_num}"
|
|
151
|
+
|
|
152
|
+
if result == "correct":
|
|
153
|
+
session["score"]["correct"] += 1
|
|
154
|
+
elif result == "partial":
|
|
155
|
+
session["score"]["partial"] += 1
|
|
156
|
+
session["score"]["correct"] += 1
|
|
157
|
+
session["auto_flagged"].append(f"Q{q_num}: {snippet}")
|
|
158
|
+
elif result == "incorrect":
|
|
159
|
+
session["score"]["incorrect"] += 1
|
|
160
|
+
session["auto_flagged"].append(f"Q{q_num}: {snippet}")
|
|
161
|
+
|
|
162
|
+
_persist_session(session_id)
|
|
163
|
+
|
|
164
|
+
score = session["score"]
|
|
165
|
+
return f"Recorded Q{q_num}: {result}. Score: {score['correct']}/{score['total']}"
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@mcp.tool()
|
|
169
|
+
def flag_for_study(session_id: str, question: str = "") -> str:
|
|
170
|
+
"""Manually flag the current question for the study pack.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
session_id: The session ID
|
|
174
|
+
question: The question text to flag
|
|
175
|
+
"""
|
|
176
|
+
session = _sessions.get(session_id) or _restore_session(session_id)
|
|
177
|
+
if not session:
|
|
178
|
+
return f"ERROR: No session found with ID '{session_id}'."
|
|
179
|
+
|
|
180
|
+
q_num = session["q_num"]
|
|
181
|
+
snippet = question[:200] if question else f"Q{q_num}"
|
|
182
|
+
entry = f"Q{q_num}: {snippet}"
|
|
183
|
+
if entry not in session["flagged"]:
|
|
184
|
+
session["flagged"].append(entry)
|
|
185
|
+
_persist_session(session_id)
|
|
186
|
+
return f"Flagged Q{q_num} for study. Total flagged: {len(session['flagged'])}."
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@mcp.tool()
|
|
190
|
+
def end_session(session_id: str) -> str:
|
|
191
|
+
"""End the session. Returns the score and flagged topics so the AI can generate a study plan and study pack.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
session_id: The session ID
|
|
195
|
+
"""
|
|
196
|
+
session = _sessions.get(session_id) or _restore_session(session_id)
|
|
197
|
+
if not session:
|
|
198
|
+
return f"ERROR: No session found with ID '{session_id}'."
|
|
199
|
+
|
|
200
|
+
all_flagged = list(dict.fromkeys(session["auto_flagged"] + session["flagged"]))
|
|
201
|
+
session["ended"] = True
|
|
202
|
+
session["all_flagged"] = all_flagged
|
|
203
|
+
_persist_session(session_id)
|
|
204
|
+
|
|
205
|
+
score = session["score"]
|
|
206
|
+
topics_list = "\n".join(f" • {t}" for t in all_flagged) if all_flagged else " (none — full score!)"
|
|
207
|
+
|
|
208
|
+
return "\n".join([
|
|
209
|
+
"=== SESSION ENDED ===",
|
|
210
|
+
f"Score: {score['correct']}/{score['total']} "
|
|
211
|
+
f"({score['incorrect']} incorrect, {score['partial']} partial)",
|
|
212
|
+
"",
|
|
213
|
+
"Auto-detected study topics:",
|
|
214
|
+
topics_list,
|
|
215
|
+
"",
|
|
216
|
+
"Generate a Study Plan now (see skill rules), then ask the user:",
|
|
217
|
+
' "Want to add any extra topics to your study pack before I save it?"',
|
|
218
|
+
f"Then call save_study_pack(session_id='{session_id}', name=<chosen name>, content=<generated pack>)",
|
|
219
|
+
])
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@mcp.tool()
|
|
223
|
+
def save_study_pack(session_id: str, name: str, content: str) -> str:
|
|
224
|
+
"""Save the study pack content to disk.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
session_id: The session ID (must have called end_session first)
|
|
228
|
+
name: Slug name for this pack, e.g. "aws-saa-week1" or "python-interview-june"
|
|
229
|
+
content: The full study pack markdown generated by the AI
|
|
230
|
+
"""
|
|
231
|
+
session = _sessions.get(session_id) or _restore_session(session_id)
|
|
232
|
+
if not session:
|
|
233
|
+
return f"ERROR: No session found with ID '{session_id}'."
|
|
234
|
+
if not session.get("ended"):
|
|
235
|
+
return "ERROR: Call end_session first before saving a study pack."
|
|
236
|
+
|
|
237
|
+
safe_name = re.sub(r"[^a-z0-9_-]", "-", name.lower().strip())
|
|
238
|
+
if not safe_name:
|
|
239
|
+
return "ERROR: Pack name must contain at least one letter or number."
|
|
240
|
+
|
|
241
|
+
packs = _packs_dir()
|
|
242
|
+
pack_json_path = packs / f"{safe_name}.json"
|
|
243
|
+
pack_md_path = packs / f"{safe_name}.md"
|
|
244
|
+
|
|
245
|
+
pack_data = {
|
|
246
|
+
"name": safe_name,
|
|
247
|
+
"mode": session["mode"],
|
|
248
|
+
"score": session["score"],
|
|
249
|
+
"topics": session.get("all_flagged", []),
|
|
250
|
+
"raw_markdown": content,
|
|
251
|
+
}
|
|
252
|
+
pack_json_path.write_text(json.dumps(pack_data, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
253
|
+
pack_md_path.write_text(f"# Study Pack: {safe_name}\n\n{content}", encoding="utf-8")
|
|
254
|
+
|
|
255
|
+
_delete_session(session_id)
|
|
256
|
+
|
|
257
|
+
return (
|
|
258
|
+
f"Study pack '{safe_name}' saved.\n"
|
|
259
|
+
f" JSON → {pack_json_path}\n"
|
|
260
|
+
f" Markdown → {pack_md_path}"
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
@mcp.tool()
|
|
265
|
+
def list_sessions() -> str:
|
|
266
|
+
"""List all saved sessions (pending and completed).
|
|
267
|
+
|
|
268
|
+
IMPORTANT: Call this at the start of every new conversation before doing anything else.
|
|
269
|
+
If there are pending (non-ended) sessions, ask the user whether they want to resume one
|
|
270
|
+
or start a new session. Use resume_session(session_id) to continue a pending session.
|
|
271
|
+
"""
|
|
272
|
+
sessions_dir = _sessions_dir()
|
|
273
|
+
files = sorted(sessions_dir.glob("*.json"), key=lambda f: f.stat().st_mtime, reverse=True)
|
|
274
|
+
if not files:
|
|
275
|
+
return "No saved sessions found. Use start_session to begin."
|
|
276
|
+
|
|
277
|
+
lines = [f"Saved sessions ({len(files)}):\n"]
|
|
278
|
+
for f in files:
|
|
279
|
+
try:
|
|
280
|
+
data = json.loads(f.read_text(encoding="utf-8"))
|
|
281
|
+
session_id = f.stem
|
|
282
|
+
mode = data.get("mode", "?")
|
|
283
|
+
cert = data.get("cert_name", "")
|
|
284
|
+
score = data.get("score", {})
|
|
285
|
+
ended = data.get("ended", False)
|
|
286
|
+
status = "completed" if ended else "IN PROGRESS"
|
|
287
|
+
label = f"{mode}" + (f" — {cert}" if cert else "")
|
|
288
|
+
lines.append(
|
|
289
|
+
f" • {session_id} [{label}] "
|
|
290
|
+
f"score: {score.get('correct','?')}/{score.get('total','?')} [{status}]"
|
|
291
|
+
)
|
|
292
|
+
except Exception:
|
|
293
|
+
lines.append(f" • {f.stem} [unreadable]")
|
|
294
|
+
|
|
295
|
+
pending = sum(
|
|
296
|
+
1 for f in files
|
|
297
|
+
if not json.loads(f.read_text(encoding="utf-8")).get("ended", False)
|
|
298
|
+
)
|
|
299
|
+
if pending:
|
|
300
|
+
lines.append(f"\n{pending} session(s) in progress — use resume_session(session_id) to continue one.")
|
|
301
|
+
return "\n".join(lines)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
@mcp.tool()
|
|
305
|
+
def resume_session(session_id: str) -> str:
|
|
306
|
+
"""Resume a previously saved session. Returns full session state and skill rules.
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
session_id: The session ID to resume (from list_sessions)
|
|
310
|
+
"""
|
|
311
|
+
session = _restore_session(session_id)
|
|
312
|
+
if not session:
|
|
313
|
+
return f"ERROR: No session found with ID '{session_id}'. Use list_sessions to see available sessions."
|
|
314
|
+
if session.get("ended"):
|
|
315
|
+
return f"ERROR: Session '{session_id}' has already ended. Use start_session to begin a new one."
|
|
316
|
+
|
|
317
|
+
mode = session["mode"]
|
|
318
|
+
skill = _load_skill(mode)
|
|
319
|
+
score = session["score"]
|
|
320
|
+
cert = session.get("cert_name", "")
|
|
321
|
+
q_num = session.get("q_num", 0)
|
|
322
|
+
auto_flagged = session.get("auto_flagged", [])
|
|
323
|
+
|
|
324
|
+
context_lines = [
|
|
325
|
+
f"Session ID: {session_id}",
|
|
326
|
+
f"Mode: {mode}",
|
|
327
|
+
]
|
|
328
|
+
if cert:
|
|
329
|
+
context_lines.append(f"Certification: {cert}")
|
|
330
|
+
context_lines += [
|
|
331
|
+
f"Questions answered so far: {q_num}",
|
|
332
|
+
f"Score so far: {score['correct']}/{score['total']} "
|
|
333
|
+
f"({score['incorrect']} incorrect, {score['partial']} partial)",
|
|
334
|
+
]
|
|
335
|
+
if auto_flagged:
|
|
336
|
+
context_lines.append("Topics flagged for study so far:")
|
|
337
|
+
for t in auto_flagged:
|
|
338
|
+
context_lines.append(f" • {t}")
|
|
339
|
+
|
|
340
|
+
return "\n".join([
|
|
341
|
+
"=== STACKPREP SESSION RESUMED ===",
|
|
342
|
+
"\n".join(context_lines),
|
|
343
|
+
"\n=== SKILL RULES (follow these exactly) ===",
|
|
344
|
+
skill,
|
|
345
|
+
])
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
@mcp.tool()
|
|
349
|
+
def list_study_packs() -> str:
|
|
350
|
+
"""List all saved study packs."""
|
|
351
|
+
packs = _packs_dir()
|
|
352
|
+
files = sorted(packs.glob("*.json"))
|
|
353
|
+
if not files:
|
|
354
|
+
return f"No study packs saved yet. Packs are stored in: {packs}"
|
|
355
|
+
|
|
356
|
+
lines = [f"Saved study packs ({len(files)}) — {packs}\n"]
|
|
357
|
+
for f in files:
|
|
358
|
+
try:
|
|
359
|
+
data = json.loads(f.read_text(encoding="utf-8"))
|
|
360
|
+
mode = data.get("mode", "?")
|
|
361
|
+
score = data.get("score", {})
|
|
362
|
+
lines.append(f" • {f.stem} [{mode}] score: {score.get('correct','?')}/{score.get('total','?')}")
|
|
363
|
+
except Exception:
|
|
364
|
+
lines.append(f" • {f.stem}")
|
|
365
|
+
return "\n".join(lines)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
@mcp.tool()
|
|
369
|
+
def load_study_pack(name: str) -> str:
|
|
370
|
+
"""Load a previously saved study pack by name.
|
|
371
|
+
|
|
372
|
+
Args:
|
|
373
|
+
name: The pack name (e.g. "aws-saa-week1"). Use list_study_packs to see available packs.
|
|
374
|
+
"""
|
|
375
|
+
safe_name = re.sub(r"[^a-z0-9_-]", "-", name.lower().strip())
|
|
376
|
+
pack_path = _packs_dir() / f"{safe_name}.json"
|
|
377
|
+
if not pack_path.exists():
|
|
378
|
+
return f"ERROR: No study pack named '{safe_name}' found. Use list_study_packs to see available packs."
|
|
379
|
+
|
|
380
|
+
data = json.loads(pack_path.read_text(encoding="utf-8"))
|
|
381
|
+
return data.get("raw_markdown", json.dumps(data, indent=2))
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def main() -> None:
|
|
385
|
+
mcp.run()
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
if __name__ == "__main__":
|
|
389
|
+
main()
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: stackprep-certification
|
|
3
|
+
description: Certification prep skill for the stackprep MCP server. Activated when mode is "certification". Drives question generation, scoring, adaptive difficulty, and study pack creation.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# stackprep — Certification Mode
|
|
7
|
+
|
|
8
|
+
Adaptive certification exam prep — one question at a time, with instant feedback and doc links.
|
|
9
|
+
|
|
10
|
+
## Session setup
|
|
11
|
+
|
|
12
|
+
Inputs arrive via MCP (certification name, extra topics). Give a short 2–3 line summary:
|
|
13
|
+
- Exam structure overview
|
|
14
|
+
- Key domains and weightings from the latest official exam guide
|
|
15
|
+
|
|
16
|
+
Then wait — questions are requested one at a time via `next_question`.
|
|
17
|
+
|
|
18
|
+
## Question format
|
|
19
|
+
|
|
20
|
+
Generate ONE question per turn. Never generate multiple questions at once.
|
|
21
|
+
|
|
22
|
+
**Always multiple choice:**
|
|
23
|
+
```
|
|
24
|
+
Q. [Domain: <domain name>] <question text>
|
|
25
|
+
a) …
|
|
26
|
+
b) …
|
|
27
|
+
c) …
|
|
28
|
+
d) …
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Do NOT include the answer in the question. Only reveal the correct answer in the EXPLANATION after the user has replied.
|
|
32
|
+
|
|
33
|
+
## Scoring (after each answer)
|
|
34
|
+
|
|
35
|
+
Keep scoring SHORT — 2 sentences max.
|
|
36
|
+
|
|
37
|
+
- ⚠️ Partial = ✅ Correct. Note one improvement in 1 sentence.
|
|
38
|
+
- ❌ Only if clearly wrong.
|
|
39
|
+
|
|
40
|
+
Always use this exact structure:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
RESULT: ✅ Correct OR RESULT: ⚠️ Partial OR RESULT: ❌ Incorrect
|
|
44
|
+
|
|
45
|
+
EXPLANATION: <2 sentences max>
|
|
46
|
+
|
|
47
|
+
DOCS: <Title>: <url>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Always include 1 real, publicly accessible URL relevant to the topic (official docs, RFC, vendor channel).
|
|
51
|
+
|
|
52
|
+
After scoring, ask:
|
|
53
|
+
**"Next question? [Y] — or type S to save a Study Pack, X to exit"**
|
|
54
|
+
|
|
55
|
+
## Adaptive difficulty
|
|
56
|
+
|
|
57
|
+
- Track topics the user gets wrong — weight subsequent questions toward those topics
|
|
58
|
+
- Gradually increase difficulty on topics answered correctly
|
|
59
|
+
- Never repeat the same question in a session
|
|
60
|
+
|
|
61
|
+
## Study Pack
|
|
62
|
+
|
|
63
|
+
A study pack is ALWAYS offered at the end of every session, regardless of score.
|
|
64
|
+
|
|
65
|
+
The MCP server automatically tracks all ❌ Incorrect and ⚠️ Partial answers as study topics — the user does not need to flag them manually. If the user scored 100%, still offer the pack to reinforce the topics covered.
|
|
66
|
+
|
|
67
|
+
When `end_session` is called, the server shows the auto-detected topics and asks:
|
|
68
|
+
> "Want to add any extra topics before I generate it?"
|
|
69
|
+
|
|
70
|
+
The user can say no or list extras, then call `save_study_pack` with a chosen name to persist it to disk.
|
|
71
|
+
|
|
72
|
+
**Pack format** — produce a JSON block first, then a markdown summary:
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
[
|
|
76
|
+
{
|
|
77
|
+
"topic": "",
|
|
78
|
+
"official_docs": [{"title": "", "url": ""}],
|
|
79
|
+
"videos": [{"title": "", "url": ""}],
|
|
80
|
+
"exam_prep": [{"title": "", "url": ""}],
|
|
81
|
+
"summary": ""
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Use only real, publicly accessible URLs. Never fabricate documentation links.
|
|
87
|
+
|
|
88
|
+
## Exit / Study Plan
|
|
89
|
+
|
|
90
|
+
When `end_session` is called, produce a **Study Plan**:
|
|
91
|
+
- Topics mastered (≥ 80% correct)
|
|
92
|
+
- Topics to review (50–79%)
|
|
93
|
+
- Topics to focus on (< 50%)
|
|
94
|
+
- 3–5 concrete study actions per weak area
|
|
95
|
+
|
|
96
|
+
## Quality rules
|
|
97
|
+
|
|
98
|
+
- Questions must reflect the latest stable documentation for every technology
|
|
99
|
+
- Never repeat identical questions in the same session
|
|
100
|
+
- For cert prep, always cite the exam domain and use the latest official exam guide
|
|
101
|
+
- Only use real, publicly accessible URLs
|
|
102
|
+
- **CRITICAL — answer position**: The correct answer MUST be placed at a genuinely random position (a, b, c, or d). Spread correct answers across all four letters throughout a session. Never default to b or c. If the last two questions shared the same correct letter, force a different one now.
|
|
103
|
+
- Never use "all of the above" or "none of the above" unless it is the only honest way to test the concept.
|
|
104
|
+
|
|
105
|
+
## Certifications with limited official documentation
|
|
106
|
+
|
|
107
|
+
For newer or niche certifications (e.g. NVIDIA Agentic AI, emerging vendor certs) where the official exam guide is sparse:
|
|
108
|
+
- Draw questions from course learning objectives and community exam reports (Reddit, Discord, LinkedIn posts from recent candidates).
|
|
109
|
+
- Cite community sources in DOCS when no official docs exist.
|
|
110
|
+
- Do NOT fabricate official documentation URLs.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: stackprep-interview
|
|
3
|
+
description: Interview prep skill for the stackprep MCP server. Activated when mode is "interview". Drives question generation, scoring, adaptive difficulty, and study pack creation.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# stackprep — Interview Mode
|
|
7
|
+
|
|
8
|
+
Adaptive technical interview prep — one question at a time, with instant feedback and doc links.
|
|
9
|
+
|
|
10
|
+
## Session setup
|
|
11
|
+
|
|
12
|
+
Inputs arrive via MCP (CV, job description, extra topics). Give a short 2–3 line summary:
|
|
13
|
+
- Seniority level inferred from CV
|
|
14
|
+
- Key domains
|
|
15
|
+
- Top skill gaps vs. the job description
|
|
16
|
+
|
|
17
|
+
Then wait — questions are requested one at a time via `next_question`.
|
|
18
|
+
|
|
19
|
+
## Question format
|
|
20
|
+
|
|
21
|
+
Generate ONE question per turn. Never generate multiple questions at once.
|
|
22
|
+
|
|
23
|
+
**Open-ended** (only for simple one-sentence answers):
|
|
24
|
+
```
|
|
25
|
+
Q. [Conceptual | Scenario | Code | Gotcha] <question text>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Multiple choice** (when the topic requires a long or detailed answer):
|
|
29
|
+
```
|
|
30
|
+
Q. [<domain>] <question text>
|
|
31
|
+
a) …
|
|
32
|
+
b) …
|
|
33
|
+
c) …
|
|
34
|
+
d) …
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Do NOT include the answer in the question. Only reveal the correct answer in the EXPLANATION after the user has replied.
|
|
38
|
+
|
|
39
|
+
## Scoring (after each answer)
|
|
40
|
+
|
|
41
|
+
Keep scoring SHORT — 2 sentences max.
|
|
42
|
+
|
|
43
|
+
- ⚠️ Partial = ✅ Correct. Note one improvement in 1 sentence.
|
|
44
|
+
- ❌ Only if clearly wrong.
|
|
45
|
+
|
|
46
|
+
Always use this exact structure:
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
RESULT: ✅ Correct OR RESULT: ⚠️ Partial OR RESULT: ❌ Incorrect
|
|
50
|
+
|
|
51
|
+
EXPLANATION: <2 sentences max>
|
|
52
|
+
|
|
53
|
+
DOCS: <Title>: <url>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Always include 1 real, publicly accessible URL relevant to the topic (official docs, RFC, vendor channel).
|
|
57
|
+
|
|
58
|
+
After scoring, ask:
|
|
59
|
+
**"Next question? [Y] — or type S to save a Study Pack, X to exit"**
|
|
60
|
+
|
|
61
|
+
## Adaptive difficulty
|
|
62
|
+
|
|
63
|
+
- Track topics the user gets wrong — weight subsequent questions toward those topics
|
|
64
|
+
- Gradually increase difficulty on topics answered correctly
|
|
65
|
+
- Never repeat the same question in a session
|
|
66
|
+
|
|
67
|
+
## Study Pack
|
|
68
|
+
|
|
69
|
+
A study pack is ALWAYS offered at the end of every session, regardless of score.
|
|
70
|
+
|
|
71
|
+
The MCP server automatically tracks all ❌ Incorrect and ⚠️ Partial answers as study topics — the user does not need to flag them manually. If the user scored 100%, still offer the pack to reinforce the topics covered.
|
|
72
|
+
|
|
73
|
+
When `end_session` is called, the server shows the auto-detected topics and asks:
|
|
74
|
+
> "Want to add any extra topics before I generate it?"
|
|
75
|
+
|
|
76
|
+
The user can say no or list extras, then call `save_study_pack` with a chosen name to persist it to disk.
|
|
77
|
+
|
|
78
|
+
**Pack format** — produce a JSON block first, then a markdown summary:
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
[
|
|
82
|
+
{
|
|
83
|
+
"topic": "",
|
|
84
|
+
"official_docs": [{"title": "", "url": ""}],
|
|
85
|
+
"videos": [{"title": "", "url": ""}],
|
|
86
|
+
"exam_prep": [{"title": "", "url": ""}],
|
|
87
|
+
"summary": ""
|
|
88
|
+
}
|
|
89
|
+
]
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Use only real, publicly accessible URLs. Never fabricate documentation links.
|
|
93
|
+
|
|
94
|
+
## Exit / Study Plan
|
|
95
|
+
|
|
96
|
+
When `end_session` is called, produce a **Study Plan**:
|
|
97
|
+
- Topics mastered (≥ 80% correct)
|
|
98
|
+
- Topics to review (50–79%)
|
|
99
|
+
- Topics to focus on (< 50%)
|
|
100
|
+
- 3–5 concrete study actions per weak area
|
|
101
|
+
|
|
102
|
+
## Quality rules
|
|
103
|
+
|
|
104
|
+
- Questions must reflect the latest stable documentation for every technology
|
|
105
|
+
- Never repeat identical questions in the same session
|
|
106
|
+
- For SQL/code, specify dialect/runtime (e.g. PostgreSQL 16, Python 3.12, dbt 1.8)
|
|
107
|
+
- Only use real, publicly accessible URLs
|
|
108
|
+
- **CRITICAL — answer position**: The correct answer MUST be placed at a genuinely random position (a, b, c, or d). Spread correct answers across all four letters throughout a session. Never default to b or c. If the last two questions shared the same correct letter, force a different one now.
|
|
109
|
+
- Never use "all of the above" or "none of the above" unless it is the only honest way to test the concept.
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: stackprep-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: stackprep as an MCP server — interview & certification prep for any AI client
|
|
5
|
+
Project-URL: Homepage, https://github.com/youngpada1/stackprep-mcp
|
|
6
|
+
Project-URL: Repository, https://github.com/youngpada1/stackprep-mcp
|
|
7
|
+
Author-email: Flavia Fauconnet <flavsferr@gmail.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: ai,certification,interview,mcp,prep
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Requires-Dist: mcp[cli]>=1.0.0
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# stackprep-mcp
|
|
22
|
+
|
|
23
|
+
> stackprep as an MCP server — interview & certification prep for any AI client
|
|
24
|
+
|
|
25
|
+
Works with any MCP-compatible client: **Claude Code**, **Cursor**, **Cline**, **Windsurf**, **Continue.dev**, **Codex CLI**, and more. No API key required — your existing AI subscription does the work.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## What it does
|
|
30
|
+
|
|
31
|
+
stackprep-mcp is a pure state-management MCP server. It tracks your session and study packs on disk; your AI client (Claude, Cursor, Codex, etc.) handles all the question generation and scoring logic using the skill rules returned at session start.
|
|
32
|
+
|
|
33
|
+
- One question at a time — interview or certification mode
|
|
34
|
+
- Instant scoring with doc links after every answer
|
|
35
|
+
- Auto-detects wrong/partial answers and builds a named study pack
|
|
36
|
+
- Sessions and study packs saved to disk — resume anytime, sync via iCloud
|
|
37
|
+
- Resume in-progress sessions across conversations or devices
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
Clone the repo and install dependencies with `uv`:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
git clone https://github.com/youngpada1/stackprep-mcp
|
|
47
|
+
cd stackprep-mcp
|
|
48
|
+
uv sync
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
> Requires [uv](https://docs.astral.sh/uv/) and Python 3.11+.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Configure your MCP client
|
|
56
|
+
|
|
57
|
+
### Claude Code
|
|
58
|
+
|
|
59
|
+
Create `.mcp.json` in your project root:
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"mcpServers": {
|
|
64
|
+
"stackprep": {
|
|
65
|
+
"command": "uv",
|
|
66
|
+
"args": ["run", "--directory", "/path/to/stackprep-mcp", "stackprep-mcp"]
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Cursor
|
|
73
|
+
|
|
74
|
+
Create `~/.cursor/mcp.json`:
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
{
|
|
78
|
+
"mcpServers": {
|
|
79
|
+
"stackprep": {
|
|
80
|
+
"command": "uv",
|
|
81
|
+
"args": ["run", "--directory", "/path/to/stackprep-mcp", "stackprep-mcp"]
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Then open Cursor → **Cmd+Shift+J** → MCP tab — stackprep should appear with a green dot.
|
|
88
|
+
|
|
89
|
+
### Codex CLI
|
|
90
|
+
|
|
91
|
+
Add to `~/.codex/config.yaml`:
|
|
92
|
+
|
|
93
|
+
```yaml
|
|
94
|
+
mcpServers:
|
|
95
|
+
stackprep:
|
|
96
|
+
command: uv
|
|
97
|
+
args:
|
|
98
|
+
- run
|
|
99
|
+
- --directory
|
|
100
|
+
- /path/to/stackprep-mcp
|
|
101
|
+
- stackprep-mcp
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Replace `/path/to/stackprep-mcp` with the absolute path to the cloned repo.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Study pack storage
|
|
109
|
+
|
|
110
|
+
Study packs and sessions are saved to `~/.stackprep/` by default.
|
|
111
|
+
|
|
112
|
+
**Sync across devices with iCloud** (recommended on macOS):
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
# Add to ~/.zshrc or ~/.zprofile
|
|
116
|
+
export STACKPREP_PACKS_DIR="$HOME/Documents/stackprep-packs"
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`~/Documents` is synced to iCloud by default on macOS (requires iCloud Drive > Desktop & Documents enabled). Your packs will be available on any Mac signed into your Apple ID — and readable via the Files app on iPhone.
|
|
120
|
+
|
|
121
|
+
**Custom path:**
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
export STACKPREP_PACKS_DIR="/path/to/your/folder"
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Point this at any Dropbox, Google Drive, or OneDrive folder for cross-platform sync.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Environment variables
|
|
132
|
+
|
|
133
|
+
| Variable | Default | Description |
|
|
134
|
+
|---|---|---|
|
|
135
|
+
| `STACKPREP_PACKS_DIR` | `~/.stackprep` | Root directory for packs and sessions. |
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Skills (modes)
|
|
140
|
+
|
|
141
|
+
| Mode | Description |
|
|
142
|
+
|---|---|
|
|
143
|
+
| `certification` | description: Certification prep skill for the stackprep MCP server. Activated when mode is "certification". Drives question generation, scoring, adaptive difficulty, and study pack creation. |
|
|
144
|
+
| `interview` | description: Interview prep skill for the stackprep MCP server. Activated when mode is "interview". Drives question generation, scoring, adaptive difficulty, and study pack creation. |
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Tools
|
|
149
|
+
|
|
150
|
+
| Tool | Description | Args |
|
|
151
|
+
|---|---|---|
|
|
152
|
+
| `start_session` | Start a new stackprep session. Returns a session ID and the skill rules for the AI to follow. | `mode`, `cert_name`, `cv`, `jd`, `extra_topics` |
|
|
153
|
+
| `submit_answer` | Record the result of an answered question. | `session_id`, `result`, `question` |
|
|
154
|
+
| `flag_for_study` | Manually flag the current question for the study pack. | `session_id`, `question` |
|
|
155
|
+
| `end_session` | End the session. Returns the score and flagged topics so the AI can generate a study plan and study pack. | `session_id` |
|
|
156
|
+
| `save_study_pack` | Save the study pack content to disk. | `session_id`, `name`, `content` |
|
|
157
|
+
| `list_sessions` | List all saved sessions (pending and completed). | |
|
|
158
|
+
| `resume_session` | Resume a previously saved session. Returns full session state and skill rules. | `session_id` |
|
|
159
|
+
| `list_study_packs` | List all saved study packs. | |
|
|
160
|
+
| `load_study_pack` | Load a previously saved study pack by name. | `name` |
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Session flow
|
|
165
|
+
|
|
166
|
+
**Certification:**
|
|
167
|
+
```
|
|
168
|
+
list_sessions() ← always called first
|
|
169
|
+
→ start_session(mode="certification", cert_name="AWS SAA-C03")
|
|
170
|
+
→ [AI generates questions one at a time]
|
|
171
|
+
→ submit_answer(session_id, result="correct"|"partial"|"incorrect", question="...")
|
|
172
|
+
→ ... repeat ...
|
|
173
|
+
→ end_session(session_id)
|
|
174
|
+
→ save_study_pack(session_id, name="aws-saa-week1", content="...")
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**Interview:**
|
|
178
|
+
```
|
|
179
|
+
list_sessions() ← always called first
|
|
180
|
+
→ start_session(mode="interview", cv="...", jd="...")
|
|
181
|
+
→ [AI generates questions one at a time]
|
|
182
|
+
→ submit_answer(session_id, result="correct"|"partial"|"incorrect", question="...")
|
|
183
|
+
→ ... repeat ...
|
|
184
|
+
→ end_session(session_id)
|
|
185
|
+
→ save_study_pack(session_id, name="python-interview-june", content="...")
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**Resuming a session:**
|
|
189
|
+
```
|
|
190
|
+
list_sessions() → shows in-progress sessions
|
|
191
|
+
→ resume_session(session_id) → loads state + skill rules, continues where you left off
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
**Loading a saved study pack:**
|
|
195
|
+
```
|
|
196
|
+
list_study_packs()
|
|
197
|
+
→ load_study_pack(name="aws-saa-week1")
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## Session persistence
|
|
203
|
+
|
|
204
|
+
Every session is saved to disk on every update. At the start of each new conversation the AI automatically calls `list_sessions` and asks whether you want to resume an in-progress session or start a new one. Sessions are stored in `~/.stackprep/sessions/` (or your custom `STACKPREP_PACKS_DIR`).
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## Also available as a Claude Code plugin
|
|
209
|
+
|
|
210
|
+
For Claude Projects or direct Claude.ai use, the behaviour rules are also available as a standalone skill file at [plugins/stackprep](https://github.com/youngpada1/stackprep/tree/main/plugins/stackprep) — no install needed.
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## Contributing / Development
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
git clone https://github.com/youngpada1/stackprep-mcp
|
|
218
|
+
cd stackprep-mcp
|
|
219
|
+
|
|
220
|
+
# Install dependencies
|
|
221
|
+
uv sync
|
|
222
|
+
|
|
223
|
+
# Activate the pre-commit hook (auto-regenerates README on every commit)
|
|
224
|
+
git config core.hooksPath .githooks
|
|
225
|
+
|
|
226
|
+
# Run the server locally
|
|
227
|
+
uv run stackprep-mcp
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
The README is auto-generated from `server.py` tool definitions and the skills files in `src/stackprep_mcp/skills/`.
|
|
231
|
+
To regenerate manually:
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
uv run python scripts/generate_readme.py
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## License
|
|
240
|
+
|
|
241
|
+
MIT — [Flavia Fauconnet](https://github.com/flavsferr)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
stackprep_mcp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
stackprep_mcp/server.py,sha256=Mt4c2iLFaS7M-hOd2JRY5Ua__5lRbTw0Vv-dGxLDA44,13353
|
|
3
|
+
stackprep_mcp/skills/certification.md,sha256=8YcxfUM9coY93gHQVLPTgmzzptg3621p3mF9EwNkeME,3982
|
|
4
|
+
stackprep_mcp/skills/interview.md,sha256=DmzfwO4xMDi7OfZFfIAFswQdGO--WQtuFvaeLcPT0DU,3698
|
|
5
|
+
stackprep_mcp-0.1.0.dist-info/METADATA,sha256=xf2k18DvHkzi69FX5prWBYVoDkMLmUmdlfQS917Bcqw,7397
|
|
6
|
+
stackprep_mcp-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
7
|
+
stackprep_mcp-0.1.0.dist-info/entry_points.txt,sha256=XYbSPfESAJ4JDMyc2UZd9Vt8HsDzr3X6XLPT1Wz-ER8,60
|
|
8
|
+
stackprep_mcp-0.1.0.dist-info/RECORD,,
|