How Hirenix teaches
One chapter. 90 minutes.
Interview-ready.
Every concept starts with a real-world problem — the kind that actually shows up in production code. Nothing to cram; it just clicks. Every question comes with a model answer: exactly what to say in the room, and why. Then an AI mock interview on the same chapter.
- 📖Concept in 5 minutesNo jargon — straight to the point
- 🛠️Real-world problemThe kind production code throws at you
- 💬Model answerExactly what to say in the room
- 🧠FlashcardsRevise in 10 minutes
- 🤖AI mock interviewIt asks follow-ups too
- 📊Weak topicsSee exactly where you're stuck

The difference isn’t the content — it’s the filter. Only what’s actually used in production and actually asked in interviews. Textbook topics the industry never touches don’t make the cut.
What you’ll learn
- ●Why MCP exists
- ●MCP vs function calling
- Host, client and serverFree account
- Your first MCP serverFree account
- Tools in depthFree account
- Resources and promptsFree account
- Who controls whatFree account
- JSON-RPC on the wireFree account
- Writing an MCP clientFree account
- Discovery and capabilitiesFree account
- Transports: stdio vs HTTPFree account
- Notifications and subscriptionsFree account
- Client primitives, and what got deprecatedFree account
- Connecting to a real host, and debuggingFree account
- Errors and tool designFree account
- Prompt injection and the trust boundaryFree account
- Auth and remote exposureFree account
- ●Project: a resume-analyzer MCP server
- RecapFree account
Why MCP exists
Picture an electrician who, every time a new appliance arrives, wires a brand-new socket just for it -- one shape for the toaster, a different shape for the lamp, a third for the fridge. Every appliance maker ships its own plug, so every wall needs a bespoke socket carved into it for that one appliance. Bring in a second electrician (a different house) and the whole job repeats from zero, wire by wire, for work that is functionally identical to the job finished last week.
In production, that repeated rewiring is what engineers call the M×N problem. Say you have three AI applications -- a customer-support chatbot, an internal Slack bot, and a mobile app -- and each of them needs a get_weather tool. Before MCP, get_weather gets written three separate times: once as a function registered in the chatbot's own SDK-specific tool format, once as a Slack-bot handler wired into that bot's particular event loop, once as a mobile backend endpoint called through yet another client library. The tool's actual logic -- call a weather API, parse the response, return a summary -- is identical all three times. What differs is only the wiring: three different formats, three different places the code has to live, three places a bug can be fixed in one and forgotten in the other two. Now add a second tool, send_email, and a fourth application. Three tools times four applications is twelve integrations, each hand-written, each maintained separately forever. That is M applications times N tools: M×N integrations, and the number gets worse, not better, as either side grows.
MCP's answer is to stop writing the wiring per pair and write it once per side instead. Build the weather tool as one MCP server, and it exposes itself through a single standard protocol. Every application that wants it now only has to speak that one protocol -- once -- to reach every server that speaks it back. M applications each implement the client side once, N tools each implement the server side once: M+N, not M×N. Twelve hand-wired integrations become three servers plus four clients, and the fifth application that shows up next month adds one client, not four more integrations.
The official MCP documentation describes this with its own analogy, and it is worth using because it is the standard framing every learner will meet again: MCP is like USB-C for AI applications. Before USB-C, every device needed its own cable shape -- one for charging, one for a monitor, one for a storage drive -- and every manufacturer duplicated the same wiring problem for their own hardware. USB-C did not make devices smarter; it gave every device and every accessory one connector they could all agree to speak, so a laptop, a phone and a monitor from three different companies plug into the same port without anyone renegotiating the shape of the cable. MCP does the same job for software: it does not make any model smarter, it gives every AI application and every tool one protocol they can all agree to speak.
Who governs MCP is itself worth stating precisely, because getting this wrong is the single most common interview mistake about MCP's origin. Anthropic donated MCP to the Agentic AI Foundation -- a Linux Foundation directed fund co-founded by Anthropic, Block and OpenAI -- announced 2025-12-09. MCP's specification is no longer any one company's product; it is governed by that foundation, and its ecosystem support already spans Claude, ChatGPT, VS Code and Cursor. That is the fact to reach for instead of guessing at a founding date: it is dated, verifiable, and it directly kills the "MCP is an Anthropic-only thing" misconception.
What MCP is NOT, because each of these is a real misconception a learner will meet in an interview:
- Not a model. MCP carries no weights, does no inference, and generates no text. The model is whatever LLM the host application is already using; MCP only changes how that model's chosen tool gets reached.
- Not an agent framework. MCP does not decide which tool to call, does not plan multi-step tasks, and does not own a loop. That decision is still ordinary function calling, made by the model inside the host application -- MCP only standardises where the tool the model picked actually lives.
- Not a replacement for your API. If you already have a REST or GraphQL API, MCP does not delete it or compete with it. An MCP server is typically a thin layer that calls your existing API on the tool's behalf -- your API keeps serving every non-AI client exactly as before, and the MCP server is simply one more, AI-shaped, way to reach it.
🌍 Real-world example: a company's internal ticketing API already serves its web dashboard and its mobile app. When the company wants an AI assistant to be able to "open a ticket," it does not rewrite the ticketing API -- it wraps the existing
create_ticketendpoint in one MCP server. That single server can now be reached by the company's internal AI assistant, and, months later, by a partner's AI tool too, without a single line of the original ticketing API changing.
💡 M×N problem = the integration cost of M applications each needing N tools, when every (application, tool) pair is hand-wired separately -- the cost grows as a product, not a sum. 💡 M+N = the integration cost once a shared protocol exists: each application implements the client side once, each tool implements the server side once. 💡 Standardisation layer = a shared format that lets independently-built pieces interoperate without either side knowing the other's internals -- MCP is a standardisation layer for tools, not a smarter way to build any one tool. 💡 Agentic AI Foundation = the Linux Foundation directed fund that now governs MCP's specification, formed by donation rather than kept as one company's product.
Standard definition: MCP (Model Context Protocol) is an open standard that solves the M×N integration problem -- M AI applications each needing N tools -- by giving both sides one shared protocol to speak, turning M×N hand-written integrations into M+N; it standardises where a tool lives and how it is reached, not how the model decides to call it, and it is not a model, not an agent framework, and not a replacement for the API doing the underlying work.
When to use it: you are building a tool, or a data source, that more than one AI application genuinely needs to reach -- a company-wide ticketing tool multiple internal bots will call, or a tool you intend to publish for other developers' AI apps to use. Reuse across independent clients is the entire value MCP is buying you.
When NOT to use it / Trade-off: a single tool that only your own app will ever call. Standing up an MCP server means a separate process, a protocol handshake, and a deployment to maintain -- real cost for zero reuse benefit if nobody else will ever connect a second client to it. In that case, an in-process function passed straight to the model (Ch5's function calling) is simpler, faster, and has one less moving part to keep running. Build the server when a second caller actually shows up, not before.
MCP vs function calling
Picture a skilled translator who works from a desk inside your office. When a document lands on your assistant's desk in a language they cannot read, your assistant looks at it, recognises "this needs translating", and walks it over to the translator. That noticing-and-deciding step -- spotting the need, picking the right person, handing over the right document -- is a skill your assistant already has; it does not change when the translator's desk moves.
Now picture that same translator quitting the office and opening their own independent agency down the street, with a phone line anyone in the building can call. Any other office -- one that never even knew the translator existed before -- can now dial that number, describe what they need, and get the same translation done, without knowing or caring how the translator's desk is arranged inside the agency. The translator's actual skill has not changed one bit. What changed is where they sit and who can reach them.
In production, your assistant's noticing-and-deciding step is what Ch5 called function calling: the model looks at a user's request, looks at a list of tools it has been given, and decides which one to call and with what arguments. That decision-making does not live in MCP -- MCP owns the second half of the picture, the translator moving out to their own agency. The tool moves out of your app's own code into its own standalone program (an MCP server), reachable over a protocol (JSON-RPC) instead of a plain function call, so that any app with an MCP client -- not just yours -- can use it.
The same tool, shown twice. Say you have written a tool that scores a resume against a job description. Here it is the Ch5 way, defined directly inside your app:
# Ch5-style: the tool lives INSIDE your app's own code
def score_resume(resume_text: str, job_description: str) -> int:
"""Score how well a resume matches a job description, 0-100."""
...
tools = [score_resume] # your code builds this list and hands it to the model
# the model reads the docstring + the function's signature and DECIDES
# whether to call score_resume for this request -- that step is function calling
Now the same tool, moved behind an MCP server:
# MCP-style: the SAME tool now lives in its OWN program
from mcp.server import MCPServer
mcp = MCPServer("resume-tools")
@mcp.tool()
def score_resume(resume_text: str, job_description: str) -> int:
"""Score how well a resume matches a job description, 0-100."""
...
Name exactly what changed and what did not. The function body did not change. The docstring did not change. What changed: a decorator replaced a plain list membership, this file is now a standalone program that another process connects to over a protocol instead of importing, and any app that speaks MCP -- not only the one you wrote -- can now reach score_resume, including a mock-interview app that never shared a codebase with your resume builder. When the model in either picture decides to call score_resume, it is doing the exact same thing underneath: reading a description, matching it to the request, producing arguments. That step is still function calling in both pictures -- MCP did not touch it.
🌍 Real-world example: Hirenix's resume builder has a
score_resumetool. A separate mock-interview feature also wants to score the candidate's resume before generating questions. Without MCP, the mock-interview app either re-implements the scoring logic or imports internal code from the resume builder -- fragile either way. Withscore_resumeliving behind an MCP server, both apps' models call the identical tool through their own MCP client; the scoring logic is written and fixed in exactly one place.
💡 Function calling = the model's own skill of looking at a request and a list of available tools, and deciding which one to call and with what arguments. Ch5 owns the mechanics of this; nothing about it changes in this chapter. 💡 MCP = a standard protocol for putting a tool in its own program so that any app, not just the one that wrote it, can reach it. It is a distribution mechanism, not a decision-making one.
"MCP replaces function calling" is the wrong answer, and it is the single most common mistake made about this protocol. It is wrong because the two are not alternatives to each other -- they answer different questions. Function calling answers "given this request and these tools, which one should I call?" MCP answers "where does this tool live, and who besides me is allowed to call it?" An MCP-connected model still performs function calling on every single call; MCP has simply changed where the tool it is calling happens to be running. Saying "MCP replaces function calling" is like saying a phone line replaces the translator's skill -- the number you dial and the translation itself are two entirely different things.
When to use it: reach for an MCP server when the same tool genuinely needs to be reusable by more than one application or process -- a resume-scoring tool used by both your web app and a separate CLI, a database tool you want Claude Desktop, Cursor, and your own product to all call through one maintained implementation, or a tool whose logic needs to be updated in one place without redeploying every app that uses it.
When NOT to use it / Trade-off: if exactly one app will ever call the tool and it is tightly coupled to that app's own code and data, a plain Ch5-style in-process function is the right call. Standing up an MCP server means running a separate program, managing a client connection, and paying the cost of a protocol handshake and JSON-RPC round trip for every call -- overhead a single-consumer tool gets nothing back for. Reach for MCP when the reuse is real, not because it sounds more modern.
Standard definition: MCP does not replace function calling -- the model still performs function calling, deciding which tool to call and with what arguments, exactly as it does without MCP; MCP standardises tool distribution, moving a tool out of an app's own code into its own standalone server so that any MCP-compatible application can reach the same tool through a common protocol, instead of every app re-implementing it.
Project: a resume-analyzer MCP server
What we're building: every earlier topic in this chapter built one MCP server around a small, invented demo -- word counts, a divider, a resource, a prompt. This capstone builds a server around the actual product this course sits inside: a resume-analyzer, the same job a resume builder and ATS scorer already do for a user, now exposed as three MCP tools an AI host can call. This project is FREE, and it re-derives its own ideas from the ground up -- you do not need to have read every earlier topic to follow it.
Think of a college placement cell with three desks in a row. The first desk skims a resume and circles every skill it recognizes from a known list. The second desk lays that resume next to a job posting and counts how many of the posting's required skills were already circled. The third desk hands back a short, capped note of what to add before resubmitting -- never the entire missing list, because nobody reads a forty-line to-do note, and a desk that tried to hand back everything would just slow down the whole queue behind it. Each desk does exactly one job, in a fixed order, and none of them re-checks the other's work.
In production, this maps directly onto Hirenix's own resume builder and ATS scorer: a user's resume text and a target job description are two strings a backend already has sitting in a database row, and this project's three tools are the shape those existing features could be re-exposed in in Hirenix's own backend, callable by any MCP-speaking AI host rather than only by Hirenix's own frontend code.
🌍 Real-world example: a user pastes a job posting into Hirenix's chat and asks "am I ready for this role?" An AI host with this server connected calls
score_resume, sees a low number, callssuggest_improvementsto find out why, and answers in one turn -- three tool calls chained by the model, none of them written as one big function by a human.
💡 Keyword overlap = comparing two pieces of text by which known terms appear in both, with no understanding of meaning -- the heuristic every tool in this project uses instead of a model call.
💡 Bounded result = a tool result whose size is capped by the tool itself, not left to grow with the input -- covered as a general rule in
errors-and-tool-design(topic 15); this project applies that rule rather than re-explaining it.
Standard definition: A resume-analyzer MCP server is an ordinary MCP server -- built with MCPServer, @mcp.tool() and mcp.run(), no different from any other server in this chapter -- whose tools happen to implement a real product feature (skill extraction, resume-to-job scoring, and improvement suggestions) using plain-Python heuristics instead of a model call, so the whole pipeline is runnable and testable with no API key, while still leaving an explicit, documented seam for where a model call would slot in if richer, non-keyword analysis were wanted later.
⚠ One plain sentence on what ran and what did not: every tool below ran for real, twice, against a real client over stdio, with byte-identical output on both runs -- there is no illustrative banner anywhere in this file, and no model or provider call exists in the code at all.
Step 1 -- Three tools, one client, zero API keys
Stripped to its shape, this project is: three @mcp.tool() functions on one MCPServer, and one client that calls all three in sequence. extract_skills(resume_text) reads a resume and reports which skills it recognizes. score_resume(resume_text, job_description) compares that resume against a job description and returns a 0-100 score. suggest_improvements(resume_text, job_description) is the hardened tool: it validates its own input and always returns a capped list, never an unbounded one, regardless of how large the actual gap is.
All three sit on top of one shared idea: a fixed skill vocabulary -- a plain Python list of about three dozen technical and soft skills. Every tool below is built by matching this one list against text in different combinations. There is no LLM call anywhere in this file; matching a known word against a string is something plain Python can already do, and the chapter's own accuracy rule for this topic is explicit about not reaching for a model call just because one exists.
Step 2 -- extract_skills: the shared matching logic, exposed as the first tool
def _find_skills(text: str) -> set:
lowered = text.lower()
return {skill for skill in SKILL_VOCAB if skill in lowered}
@mcp.tool()
def extract_skills(resume_text: str) -> str:
"""Extract known technical and soft skills from resume text by matching
against a fixed skill vocabulary. Returns a comma-separated list."""
if not resume_text or not resume_text.strip():
return "Error: resume_text is empty. Provide the resume's plain text."
skills = sorted(_find_skills(resume_text))
if not skills:
return "No known skills found in this resume text."
return ", ".join(skills)
_find_skills is a private helper, not a tool -- it is not decorated with @mcp.tool(), so the model never sees it and can never call it directly. It is the one piece of logic every tool in this file reuses, which is exactly why it is written once instead of copy-pasted into three functions. extract_skills itself follows the failure-handling lesson errors-and-tool-design (topic 15) already established: an empty resume_text returns a descriptive string, it does not raise, so a model that forgot to pass real resume text can read exactly what went wrong and retry with the right argument in the same turn. Verified real run, on a sample backend-developer resume: extract_skills returned aws, communication, django, docker, git, postgresql, python, rest api, sql -- nine matches. Note sql: it was matched inside PostgreSQL, not as a standalone word -- substring matching has no idea where a word starts. That cuts both ways and Step 5's limitations name both.
Step 3 -- score_resume: set intersection, not a model's judgement
@mcp.tool()
def score_resume(resume_text: str, job_description: str) -> str:
"""Score a resume against a job description, 0-100. The score is the
percentage of the job description's skills that also appear in the
resume -- a keyword-overlap heuristic, not a semantic judgement."""
resume_skills = _find_skills(resume_text)
job_skills = _find_skills(job_description)
overlap = resume_skills & job_skills
score = round(100 * len(overlap) / len(job_skills))
...
The score is nothing more than len(overlap) / len(job_skills) as a percentage -- resume_skills & job_skills is Python's own set-intersection operator, doing the entire "does this resume match this job" comparison in one line. This is a genuinely weaker signal than a model reading both texts for meaning: it cannot tell that "built REST APIs" implies the rest api skill it already matched, and it cannot give partial credit for a related-but-not-identical skill. The docstring says so in plain words -- "a keyword-overlap heuristic, not a semantic judgement" -- because a tool description that oversold its own accuracy would be actively misleading to the model reading it, which is exactly the tool-design lesson topic 15 already covered. Verified real run: against the same sample resume and a matching backend job description, score_resume returned score=64/100 matched=7/11 matched_skills=aws, communication, django, docker, postgresql, python, sql -- a real division, not a hardcoded number, and it changes if either input text changes.
Step 4 -- suggest_improvements: the hardened tool
@mcp.tool()
def suggest_improvements(resume_text: str, job_description: str) -> str:
"""Suggest up to 5 concrete skills to add to a resume ..."""
if not isinstance(resume_text, str) or not isinstance(job_description, str):
return "Error: resume_text and job_description must both be strings."
if not resume_text.strip() or not job_description.strip():
return "Error: resume_text and job_description must both be non-empty."
if len(resume_text) > MAX_TEXT_CHARS or len(job_description) > MAX_TEXT_CHARS:
return (f"Error: input too large (limit {MAX_TEXT_CHARS} characters each). "
"Trim to the relevant section and retry.")
...
bounded = missing[:MAX_SUGGESTIONS]
This is the deliberately-hardened tool, and it is hardened in two separate places for two separate reasons. Input validation first: an empty-string check and a length check, each returning a descriptive string rather than letting Python raise an AttributeError the model cannot act on. The isinstance line above them is a belt-and-braces guard for direct Python callers only -- over MCP it never fires, because the JSON Schema generated from the str type hints makes the SDK reject a non-string first, with is_error=True and a pydantic validation message. Measured: passing resume_text=12345 returns is_error=True | Error executing tool suggest_improvements: 1 validation error ... Input should be a valid string. That is worth knowing: your type hints are already a validation layer, and the checks you write by hand only cover what the schema cannot express. The length check specifically exists because this tool's own logic is O(vocab size) per input regardless of length, so a pasted 500-page document would not slow the computation down -- but it WOULD sit in the request and the eventual result, and every character of it is a character an AI host has to hold in context to make this one tool call, which is the same "results are charged to the context window" lesson from topic 15, applied here to the input side instead of the output side. The bounded result second: missing[:MAX_SUGGESTIONS] caps the suggestion list at 5 no matter how many skills are actually missing, and the returned string says explicitly how many were left out rather than silently dropping them -- so the model gets a short, actionable list plus an honest count, never an unbounded dump that crowds out the rest of the conversation.
Verified real run, three separate calls: with a normal resume and job description, the tool returned four missing skills (agile, ci/cd, graphql, kubernetes) uncapped because four is under the limit. With resume_text set to a whitespace-only string, it returned Error: resume_text and job_description must both be non-empty. -- no exception, no crash. With resume_text set to "python " repeated to 28,000 characters (past the 20,000-character limit), it returned Error: input too large (limit 20000 characters each). Trim to the relevant section and retry. -- rejected before any matching logic ran at all. And with a job description naming nineteen skills and a one-skill resume whose single skill is none of those nineteen (so all 19 are missing), the tool returned exactly 5 suggestions plus the line (14 more missing skill(s) not shown -- result capped at 5) -- the cap firing for real against a case built specifically to test it, not merely claimed.
Step 5 -- The client: three tools, six calls, one server process
The client is shown in full in the snippet above; this is the exact stdio_client + ClientSession shape writing-a-client (topic 9) established: one subprocess, one handshake, list_tools() for discovery before anything is invoked, then call_tool(name, arguments) per call, reading each reply from result.content[0].text. The client in this project makes six calls total against the one running server process: the three tools once each with realistic input, then three more calls specifically built to exercise suggest_improvements's hardening -- an empty-input call, an oversized-input call, and a many-missing-skills call. Nothing here is a new client shape; it is the same three lines from topic 9, called six times with different arguments, because a capstone's job is to use what the chapter already taught, not invent a new way to do it.
What this project proves, and what I would do differently in production
Proves: three tools built entirely on set intersection and string checks, all runnable with no API key, all exercised by a real stdio client, twice, with byte-identical output both times -- including the hardened tool's validation actually rejecting bad input and its cap actually firing on a case with more than 5 missing skills.
Does not: this is a keyword-overlap heuristic, not a resume reviewer -- it cannot judge writing quality, it cannot infer a skill from a description that never names it, it fires false positives because it matches substrings and not words -- java inside JavaScript, git inside digital, react inside reactive are all measured -- and its skill list is 37 fixed terms, not a real taxonomy. Exactly one comment in the code marks where a real product would call a model instead: inside suggest_improvements, for phrasing-level suggestions beyond keyword gaps. What I would do differently in production: swap the fixed SKILL_VOCAB list for a maintained taxonomy so it does not silently miss a skill spelled slightly differently ("react.js" vs "react"), and route suggest_improvements through that one marked seam to a model call for suggestions that go beyond "this keyword is missing" -- while keeping the deterministic keyword pass as a fast, free first filter before ever spending a model call on it -- and match on word boundaries (re.search(rf'\b{re.escape(skill)}\b', lowered)) instead of in, which alone removes every false positive above.
from mcp.server import MCPServer
mcp = MCPServer("resume-analyzer")
# A small, fixed skill vocabulary stands in for a real skills taxonomy.
# In production this list would come from a database and grow from real
# resume data -- a fixed list is enough here to prove the pipeline shape.
SKILL_VOCAB = [
"python", "javascript", "typescript", "java", "sql", "react", "next.js",
"node.js", "django", "flask", "fastapi", "docker", "kubernetes", "aws",
"gcp", "azure", "git", "rest api", "graphql", "postgresql", "mongodb",
"redis", "html", "css", "tailwind", "pandas", "numpy", "pytorch",
"tensorflow", "machine learning", "data analysis", "ci/cd", "linux",
"communication", "leadership", "agile", "scrum",
]
MAX_TEXT_CHARS = 20000 # a resume this large is almost certainly bad input, not a real resume
MAX_SUGGESTIONS = 5 # bounded so the result never floods the model's context window
def _find_skills(text: str) -> set:
"""Case-insensitive substring match of each vocab entry against text."""
lowered = text.lower()
return {skill for skill in SKILL_VOCAB if skill in lowered}
@mcp.tool()
def extract_skills(resume_text: str) -> str:
"""Extract known technical and soft skills from resume text by matching
against a fixed skill vocabulary. Returns a comma-separated list."""
if not resume_text or not resume_text.strip():
return "Error: resume_text is empty. Provide the resume's plain text."
skills = sorted(_find_skills(resume_text))
if not skills:
return "No known skills found in this resume text."
return ", ".join(skills)
@mcp.tool()
def score_resume(resume_text: str, job_description: str) -> str:
"""Score a resume against a job description, 0-100. The score is the
percentage of the job description's skills that also appear in the
resume -- a keyword-overlap heuristic, not a semantic judgement."""
if not resume_text or not resume_text.strip():
return "Error: resume_text is empty."
if not job_description or not job_description.strip():
return "Error: job_description is empty."
resume_skills = _find_skills(resume_text)
job_skills = _find_skills(job_description)
if not job_skills:
return "Error: no known skills found in job_description -- cannot score against it."
overlap = resume_skills & job_skills
score = round(100 * len(overlap) / len(job_skills))
matched = ", ".join(sorted(overlap)) if overlap else "none"
return (f"score={score}/100 matched={len(overlap)}/{len(job_skills)} "
f"matched_skills={matched}")
@mcp.tool()
def suggest_improvements(resume_text: str, job_description: str) -> str:
"""Suggest up to 5 concrete skills to add to a resume, based on skills the
job description names that the resume does not. Hardened tool: rejects
oversized or empty input instead of processing it, and always caps
the number of suggestions returned regardless of how many are missing."""
if not isinstance(resume_text, str) or not isinstance(job_description, str):
return "Error: resume_text and job_description must both be strings."
if not resume_text.strip() or not job_description.strip():
return "Error: resume_text and job_description must both be non-empty."
if len(resume_text) > MAX_TEXT_CHARS or len(job_description) > MAX_TEXT_CHARS:
return (f"Error: input too large (limit {MAX_TEXT_CHARS} characters each). "
"Trim to the relevant section and retry.")
resume_skills = _find_skills(resume_text)
job_skills = _find_skills(job_description)
missing = sorted(job_skills - resume_skills)
if not missing:
return "No missing skills found -- resume already covers every skill this job description names."
bounded = missing[:MAX_SUGGESTIONS]
omitted = len(missing) - len(bounded)
lines = [f'- Add "{s}" if you genuinely have this experience' for s in bounded]
result = "Suggested additions:\n" + "\n".join(lines)
if omitted > 0:
result += f"\n({omitted} more missing skill(s) not shown -- result capped at {MAX_SUGGESTIONS})"
# A real product would send resume_text + job_description to a model here
# for phrasing-level suggestions beyond keyword gaps -- skipped so this
# tool needs no API key and stays fully runnable offline.
return result
if __name__ == "__main__":
mcp.run()
# ---- client.py -- run this; it launches server.py as a subprocess ----
import asyncio
import sys
from mcp import StdioServerParameters
from mcp.client.stdio import stdio_client
from mcp.client.session import ClientSession
RESUME_TEXT = (
"Backend developer with 3 years of experience. Built REST API services in "
"Python using Django. Data stored in PostgreSQL. Deployed with Docker on AWS. "
"Version control with Git. Strong written communication."
)
JOB_DESCRIPTION = (
"We are hiring a backend engineer. Required: Python, Django, PostgreSQL, "
"Docker, Kubernetes, AWS, GraphQL, CI/CD, Agile delivery and clear "
"communication with the team."
)
BIG = "python " * 4000 # 28000 characters
MANY_JOB = (
"Required: aws, azure, docker, gcp, graphql, kubernetes, linux, mongodb, "
"next.js, node.js, pytorch, react, redis, scrum, tailwind, tensorflow, "
"typescript, leadership, html." # 19 skills, none of them pandas
)
SMALL_RESUME = "I know pandas."
async def main() -> None:
params = StdioServerParameters(command=sys.executable, args=["server.py"])
async with stdio_client(params) as (read, write):
async with ClientSession(read, write) as session:
init = await session.initialize()
print(f"CONNECTED: {init.server_info.name} | protocol: {init.protocol_version}")
tools = await session.list_tools()
names = [t.name for t in tools.tools]
print(f"TOOLS: {names}")
r1 = await session.call_tool("extract_skills", {"resume_text": RESUME_TEXT})
print(f"extract_skills -> is_error={r1.is_error} content={r1.content[0].text}")
r2 = await session.call_tool(
"score_resume", {"resume_text": RESUME_TEXT, "job_description": JOB_DESCRIPTION}
)
print(f"score_resume -> is_error={r2.is_error} content={r2.content[0].text}")
r3 = await session.call_tool(
"suggest_improvements",
{"resume_text": RESUME_TEXT, "job_description": JOB_DESCRIPTION},
)
print(f"suggest_improvements -> is_error={r3.is_error} content={r3.content[0].text}")
r4 = await session.call_tool(
"suggest_improvements",
{"resume_text": " ", "job_description": JOB_DESCRIPTION},
)
print(
"suggest_improvements (empty resume_text) -> "
f"is_error={r4.is_error} content={r4.content[0].text}"
)
r5 = await session.call_tool(
"suggest_improvements",
{"resume_text": BIG, "job_description": JOB_DESCRIPTION},
)
print(
"suggest_improvements (oversized resume_text, 28000 chars) -> "
f"is_error={r5.is_error} content={r5.content[0].text}"
)
r6 = await session.call_tool(
"suggest_improvements",
{"resume_text": SMALL_RESUME, "job_description": MANY_JOB},
)
print(
"suggest_improvements (many missing skills) -> "
f"is_error={r6.is_error} content={r6.content[0].text}"
)
if __name__ == "__main__":
asyncio.run(main())MCP — Model Context Protocolinterview questions & answers
10 sample questions below — 231+ in the full bank inside.
You put ./server.py as the command path in a host's config. What actually goes wrong, and what error does that produce?
A host launches your server as a subprocess from its OWN working directory, not from your project folder, so a relative path almost never resolves. It produces Python's ordinary file-not-found error -- [Errno 2] No such file or directory -- not any MCP-specific error, and the host's UI usually just shows "server failed to start" because the process never got far enough to import mcp. Fix: always write the full, absolute path to server.py in the config.
In simple terms: It's like giving someone directions from your own front door when they're actually starting from a different building -- "turn left" only works from your fixed reference point. Example: running the exact same relative-path command from one directory below where server.py lives reproduces can't open file '...\elsewhere\server.py': [Errno 2] No such file or directory immediately.
If roots, sampling, and logging are deprecated, does that mean they stop working immediately?
No. "Deprecated" flags a feature as the wrong default going forward, but it is not "removed" -- MCP's feature-lifecycle policy sets a minimum window of at least twelve months before a deprecated feature is eligible for removal, shortened only to a ninety-day floor if the feature ever becomes an active security risk. So roots, sampling, and logging/setLevel still work on a session that negotiates protocol 2025-11-25 or earlier.
In simple terms: It is like a store announcing it is discontinuing a product line -- the shelves do not empty that same day, there is a wind-down period. Example: a client that negotiates 2025-11-25 can keep calling logging/setLevel today even though it is deprecated, because the twelve-month clock only just started.
Why should you test a new MCP server with the Inspector before wiring it into any host application?
Because a host's UI gives you exactly one symptom -- "the tool isn't there" -- for at least five different underlying causes, including a genuine bug in your server. The Inspector drives your server directly over stdio with no host in the loop, so if it lists tools and answers a call correctly there, the server itself is proven and any later failure can be isolated to the host connection instead.
In simple terms: It's like a plumber bench-testing a new stove with a known-good gas line before plumbing it into the house -- if the flame doesn't light on the bench, the fault is unambiguously the stove; skip the bench and a bad first connection leaves you with two unproven things and one symptom. Example: skipping the Inspector and going straight to a host's config turns a five-minute Inspector check into twenty minutes of rereading working Python code.
What does the mcp dev <file> command actually do, and how is that different from mcp install?
mcp dev <file> runs your server standalone and launches it against the MCP Inspector -- a UI where you can list and call its tools directly, with no host application involved. mcp install is the separate step of registering the server into a host's own config so an actual host application can launch it. Check the host's current docs for exact flags, since CLI surfaces like this change.
In simple terms: Think of mcp dev as the bench rig and mcp install as finally plumbing the stove into the house -- one proves the server works in isolation, the other connects it to something real. Example: running mcp dev server.py opens the Inspector against that file directly, no claude_desktop_config.json or .mcp.json entry needed at all.
Why shouldn't you hand-roll your own token scheme for a remote MCP server, and what does the SDK give you instead?
A hand-rolled scheme is exactly where bugs like session fixation, timing leaks in token comparisons, or badly scoped tokens tend to live -- it is easy to get subtly wrong in ways that don't show up until they're exploited. The SDK already exposes the pieces an established provider needs: an MCPServer can be constructed with a token_verifier and an auth_server_provider, plus an auth settings object, so the server validates tokens issued by a provider instead of inventing its own scheme.
In simple terms: It is like wiring your own house's electrics instead of hiring a licensed electrician who follows a known code -- it might look fine on the surface while carrying mistakes only an expert would catch. Example: passing a token_verifier into MCPServer means the server delegates 'is this token valid' to code someone else has already hardened, instead of writing that check by hand.
As of the MCP 2026-07-28 protocol revision, which client primitives are deprecated, and which one is still current?
The Roots, Sampling, and Logging features are all three deprecated under SEP-2577 -- not just one or two of them. Roots and sampling are client primitives; logging is a server-side feature, declared via ServerCapabilities.logging rather than as a client capability. Elicitation is the only client primitive that is still current, though its transport also changed in this same revision.
In simple terms: Think of a hotel concierge desk that used to offer two guest-facing services -- asking your floor permissions and borrowing your phone to make a call -- plus a form-filling desk for irreversible actions; meanwhile the hotel's own PA system, not the concierge, used to shout updates down the hallway. The revised manual retired both concierge services and the PA system, keeping only the form-filling desk open. Example: a candidate who says "sampling is deprecated" and stops has only found one of three deprecated features -- roots and logging are deprecated too, even though logging sits on the hotel's side, not the concierge's.
What does the MCP specification currently point to for authenticating remote servers?
OAuth-based authorization, as of the current protocol revision -- the exact OAuth version the spec references is worth re-checking against the current spec revision before quoting one, since that detail can move independently of the protocol revision itself. The point for an interview is the direction, not memorising a version number: remote servers authenticate callers through an OAuth flow rather than each server inventing its own scheme.
In simple terms: Think of it like a building switching from 'whoever has a copy of the key' to 'show your badge at the front desk, issued by building security' -- the badge issuer is a known, established system rather than something the building invents itself. Example: a remote MCP server checks an incoming request for a valid OAuth token before running any tool, rather than trusting whoever connected.
What is elicitation for, and when should a tool reach for it?
Elicitation is how a server asks the human -- not the model -- for input mid-tool-call, before doing something risky. Reach for it specifically before a destructive or irreversible action where a wrong guess is expensive to undo, and where a human is actually available in an interactive session to answer, not a headless batch job.
In simple terms: It is the form the hotel concierge hands you before opening your room safe -- a checkpoint before something you cannot take back. Example: a close_ticket tool that elicits "Close ticket #4821, 'Payment failed', as resolved? y/n" catches the model closing the wrong ticket before it happens, not after.
What is the ONE thing that changes when you take a local stdio MCP server and expose it remotely, if the tool code inside is identical?
The reachability model inverts. A local stdio server is only reachable by a client running on the same machine over stdin/stdout, so it has no network attack surface at all -- the user's own login already does the access control, for free, and the process runs with that user's own permissions. A remote server is bound to a network address, so anyone who can route a request to it can attempt to talk to it. Proximity stops being a filter, so the server has to authenticate every request instead.
In simple terms: It is like the difference between a spare key under your own doormat and a storefront opened onto a public street -- the key is not stronger or weaker, but who can even reach the door completely changes. Example: a stdio server started as a subprocess by one developer's editor has no listening socket; the same code bound to a Streamable HTTP address is now something a stranger on the network can attempt to call.
You edit a host's config file to add a new server while the host is already running. Will the new server show up right away?
No. Every host reads its MCP config once, at startup -- the already-running process is still holding its OLD config in memory, so editing the file changes nothing until you restart. This one has nothing to reproduce standalone since it's purely the host process's own lifecycle; the fix is the same regardless of which host it is: fully quit and relaunch the application, not just reload a window or a chat tab.
In simple terms: It's like updating a printed map but expecting someone already mid-journey with the old map in their pocket to suddenly know the new route -- they only learn it once they pick up a fresh copy. Example: adding a server to .mcp.json mid-session and just switching chat tabs leaves the tool missing; only a full quit-and-relaunch of the host picks it up.
221+ more MCP — Model Context Protocol questions inside
Create a free account to read the full question bank, learn every topic, and practise with an AI mock interview.
Unlock all questions — freeReady to practise MCP — Model Context Protocol?
Unlock every topic free, then face an AI interviewer that asks follow-ups and grades your answers.