career-mcp-server
Provides tools for interacting with the GitHub REST API, including retrieving the authenticated user's profile, pinned repositories, repository listings, searching public repositories, and checking rate limit information.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@career-mcp-servershow me my GitHub profile"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
career-mcp-server
A production-grade AI Career MCP Server — a FastAPI application that exposes career-oriented tools to LLM clients via the Model Context Protocol (MCP).
v1.0.0 — the first stable release. It contains Phase 1 (foundation), Phase 2 (GitHub integration), Phase 3 (AI career intelligence & resume engine), and Phase 4 (AI platform, RAG, memory & advanced MCP). LinkedIn integration is not implemented yet — that's Phase 5, built on top of this foundation. See CHANGELOG.md and RELEASE_NOTES_v1.0.0.md for release details.
Overview
career-mcp-server is designed as a long-lived, extensible service:
A FastAPI HTTP layer for health checks, versioning, and REST endpoints.
An MCP server, built on the official MCP Python SDK, that exposes tools an LLM client can call directly.
A clean separation between API, core (cross-cutting concerns), MCP, integrations, ai, resume, career, services, models, and schemas, so future features plug in without restructuring the app.
A GitHub integration (Phase 2): an async REST client, typed Pydantic models, a caching service layer, and the same functionality exposed identically through both REST endpoints and MCP tools.
A resume & AI career intelligence engine (Phase 3): PDF upload and parsing, AI-powered analysis and scoring, resume-to-job matching, and AI-generated rewrites, cover letters, and interview prep — all behind a provider-agnostic AI abstraction, again exposed identically through REST and MCP.
An AI platform with RAG, memory & advanced MCP (Phase 4): six interchangeable LLM providers, a pluggable embeddings + vector store layer (ChromaDB), a knowledge base built from indexed resumes and GitHub repositories, retrieval-augmented chat with conversation memory and streaming, a multi-step tailored-application orchestration pipeline, and the MCP Resources and Prompts primitives alongside tools.
Phase 1 registered a single demonstration MCP tool — ping, which returns "pong" — solely to
prove the MCP integration initializes and works end-to-end. Phase 2 added 22 GitHub tools, Phase 3
added 17 more (13 resume + 4 career), and Phase 4 adds 12 more (4 RAG chat + 4 knowledge + 3
session + 1 orchestration) — 52 MCP tools in total, plus 7 MCP Resources and 8 MCP Prompts.
Related MCP server: Agent Construct
Architecture
Client (HTTP) Client (MCP)
│ │
▼ ▼
FastAPI app ───────────────────► MCPServer instance
│ middleware, exception │
│ handlers, routers │ tools / resources / prompts
▼ ▼
app/api/routers/* app/mcp/{server,registry,tools/*,resources,prompts}.py
│ │
└───────────────┬─────────────────────┘
▼
┌───────────┬────────────┬────────────┬─────────────┬───────────────┐
▼ ▼ ▼ ▼ ▼ ▼
GitHubService ResumeService CareerService RAGService KnowledgeService OrchestrationService
(caching, (storage, (advice, (chat + (indexing + (resume → skills →
validation) parsing, summaries, retrieval semantic projects → GitHub →
│ analysis, bio + memory) search) tailored resume →
▼ matching, rewriting) │ │ cover letter →
GitHubClient AI rewrite) │ │ ▼ interview prep)
│ │ │ │ VectorStore (abstract)
▼ ▼ ▼ │ │
GitHub REST ResumeStorage AIProvider (abstract) ◄─────────┘ ChromaVectorStore
API (local FS) │ │
▼ app/knowledge/chunking.py
┌──────────────┼───────────────┬──────────────────┐ (resume + GitHub
▼ ▼ ▼ ▼ document chunking)
OpenAIProvider AnthropicProvider GeminiProvider OllamaProvider
AzureProvider OpenRouterProvider EmbeddingProvider (abstract)
│
┌────────────────┼────────────────┐
▼ ▼ ▼
OpenAIEmbedding OllamaEmbedding SentenceTransformers
│
▼
SessionManager / ConversationStore (app/memory/*) ──► used by RAGService for chat history
│
▼
app/core/* (DI, exceptions, middleware, metrics, startup/shutdown, cache)
│
▼
app/config/* (settings, logging) ◄── app/services, app/models, app/schemas, app/utilsEvery REST route and MCP tool is a thin pass-through to the same service classes — there is exactly one implementation of business logic, validation, caching, and error handling per domain, regardless of which transport a client uses.
Design principles:
Dependency injection — routes and services depend on
get_settings()/get_logger()/get_github_service()/get_resume_service()/get_career_service()/get_ai_provider()/get_embedding_provider()/get_vector_store()/get_knowledge_service()/get_session_manager()/get_rag_service()/get_orchestration_service()rather than importing configuration or integrations directly.SOLID / clean architecture — each layer has one responsibility; every service inherits from the shared
BaseServicebase class so domain code stays consistent across integrations.Provider-agnostic AI — every AI-backed service depends on the abstract
AIProviderinterface (app/ai/provider.py), never on a specific SDK directly. SwappingLLM_PROVIDERswaps the implementation everywhere at once across 6 supported providers (see Supported Providers); embeddings and the vector store follow the same pattern (EmbeddingProvider,VectorStore).Lazy provider construction — AI-backed services accept a factory callable (
Callable[[], AIProvider]), not a pre-built instance, so the app and every non-AI code path work with zero API keys configured; only calling an AI-dependent method constructs (and can fail on) the provider. Applied consistently toResumeService,CareerService,KnowledgeService,RAGService, andOrchestrationService.Structured errors — every handled failure, from either transport, returns the same JSON error envelope (
{"success": false, "error": {...}}) or the equivalent MCPis_errorresult.Typed data only — no integration ever returns raw JSON or unstructured AI text; every method returns a validated Pydantic model or a list of them.
Async-first — all I/O-bound code paths are
async def, including the GitHub and AI clients.Graceful degradation — the app starts and resume storage (list/get/delete) works fully even with no
OPENAI_API_KEYconfigured; only AI-dependent calls fail, cleanly, when actually invoked. See Graceful Degradation.
Tech Stack
Layer | Technology |
Language / runtime | Python 3.12+ |
Web framework | FastAPI + Uvicorn (ASGI) |
MCP | Official MCP Python SDK — tools, resources, and prompts |
Data validation | Pydantic v2 + Pydantic Settings |
HTTP client | httpx (async, used for GitHub, Ollama, and streaming) |
Logging | Loguru (console + rotating file sinks) |
PDF parsing | pypdf |
LLM SDKs |
|
Vector database | ChromaDB (embedded/persistent mode) |
Testing | pytest, pytest-asyncio, pytest-cov |
Lint / types | Ruff (lint), mypy (strict mode) |
Containerization | Docker (multi-stage, non-root) + Docker Compose |
CI/CD | GitHub Actions |
Folder Structure
career-mcp-server/
├── app/
│ ├── main.py # FastAPI app factory + entry point
│ ├── config/
│ │ ├── settings.py # Pydantic Settings (env-driven configuration)
│ │ └── logging.py # Loguru sinks: console + rotating file logs
│ ├── api/
│ │ └── routers/
│ │ ├── health.py # GET /health
│ │ ├── github.py # 22 REST endpoints for the GitHub integration
│ │ ├── resume.py # 13 REST endpoints for resume management & AI features
│ │ ├── career.py # 4 REST endpoints for general career intelligence
│ │ ├── chat.py # POST /chat, POST /chat/stream (RAG, SSE streaming)
│ │ ├── sessions.py # POST/GET /sessions, GET/DELETE /sessions/{id}
│ │ ├── knowledge.py # POST /knowledge/{index,reindex,search}, GET /knowledge/status
│ │ ├── search.py # POST /search (top-level semantic search alias)
│ │ ├── metrics.py # GET /metrics (observability snapshot)
│ │ ├── orchestration.py # POST /orchestration/tailor-application
│ │ └── base.py # Aggregates all routers
│ ├── core/
│ │ ├── exceptions.py # Exception hierarchy + handlers
│ │ ├── dependencies.py # DI entry points (get_settings, get_logger)
│ │ ├── cache.py # Generic in-memory TTL cache
│ │ ├── middleware.py # Request ID, timing, logging, CORS, metrics, etc.
│ │ ├── metrics.py # Dependency-free counters/histograms + GET /metrics
│ │ ├── startup.py # Graceful startup hook
│ │ └── shutdown.py # Graceful shutdown hook
│ ├── mcp/
│ │ ├── server.py # Builds the MCPServer instance, registers tools/resources/prompts
│ │ ├── registry.py # Tool registry + demo `ping` tool
│ │ ├── resources.py # 7 MCP Resources (resume, portfolio, github, career, skills, ...)
│ │ ├── prompts.py # 8 MCP Prompts (reusable prompt templates for MCP clients)
│ │ └── tools/
│ │ ├── github_tools.py # 22 GitHub MCP tools (thin wrappers over GitHubService)
│ │ ├── resume_tools.py # 13 resume MCP tools (thin wrappers over ResumeService)
│ │ ├── career_tools.py # 4 career MCP tools (thin wrappers over CareerService)
│ │ ├── rag_tools.py # 4 RAG chat MCP tools (thin wrappers over RAGService)
│ │ ├── knowledge_tools.py # 4 knowledge-base MCP tools (search, status, reindex, embed)
│ │ ├── session_tools.py # 3 conversation-session MCP tools
│ │ └── orchestration_tools.py # 1 tailored-application-pipeline MCP tool
│ ├── integrations/
│ │ └── github/
│ │ ├── client.py # Async httpx client: auth, retries, pagination, rate limits
│ │ ├── models.py # Typed GitHub resource models (User, Repository, Commit, ...)
│ │ ├── errors.py # Maps GitHub HTTP errors to application exceptions
│ │ ├── validation.py # owner/repo/pagination/search input validation
│ │ ├── service.py # GitHubService: business logic + caching
│ │ └── dependencies.py # get_github_client() / get_github_service() DI factories
│ ├── ai/
│ │ ├── provider.py # AIProvider abstract interface (complete/complete_json/stream)
│ │ ├── base_provider.py # Shared scaffolding for concrete providers
│ │ ├── openai_provider.py # OpenAIProvider + ChatCompletionsMixin (shared OpenAI-wire-format
│ │ │ # logic reused by Azure and OpenRouter)
│ │ ├── azure_provider.py # AzureProvider (Azure OpenAI)
│ │ ├── openrouter_provider.py # OpenRouterProvider
│ │ ├── anthropic_provider.py # AnthropicProvider (Claude models)
│ │ ├── gemini_provider.py # GeminiProvider (Google Gemini)
│ │ ├── ollama_provider.py # OllamaProvider (local models via plain httpx)
│ │ ├── factory.py # get_ai_provider() DI factory, keyed by LLM_PROVIDER
│ │ ├── sanitize.py # Prompt-injection mitigations: input sanitizing, content fencing
│ │ ├── prompts.py # Every prompt template used anywhere, centralized
│ │ └── embeddings/
│ │ ├── provider.py # EmbeddingProvider abstract interface (embed/embed_one)
│ │ ├── openai_embeddings.py # OpenAIEmbeddingProvider
│ │ ├── ollama_embeddings.py # OllamaEmbeddingProvider
│ │ ├── sentence_transformers_embeddings.py # Local, optional-dependency provider
│ │ └── factory.py # get_embedding_provider() DI factory, keyed by EMBEDDING_PROVIDER
│ ├── resume/
│ │ ├── models.py # Parsed-resume domain models (ContactInfo, Experience, ...)
│ │ ├── schemas.py # REST/MCP request & response schemas
│ │ ├── validators.py # Upload/filename/resume-id validation
│ │ ├── storage.py # ResumeStorage abstraction + LocalResumeStorage
│ │ ├── parser.py # PDF text extraction + AI-powered structured parsing
│ │ ├── job_parser.py # AI-powered job description parsing
│ │ ├── service.py # ResumeService: storage, analysis, matching, AI rewriting
│ │ └── dependencies.py # get_resume_service() DI factory
│ ├── career/
│ │ ├── schemas.py # REST/MCP request & response schemas
│ │ ├── service.py # CareerService: advice, summaries, bio rewriting
│ │ └── dependencies.py # get_career_service() DI factory
│ ├── vectorstore/
│ │ ├── models.py # VectorRecord, SearchResult
│ │ ├── store.py # VectorStore abstract interface
│ │ ├── chroma_store.py # ChromaVectorStore (ChromaDB, run via asyncio.to_thread)
│ │ └── factory.py # get_vector_store() DI factory, keyed by VECTOR_DB
│ ├── knowledge/
│ │ ├── chunking.py # Recursive, overlap-aware text chunking for indexing
│ │ ├── resume_indexer.py # Builds section-tagged chunks from a stored resume
│ │ ├── github_indexer.py # Builds chunks from a repository's README/languages/topics/...
│ │ ├── schemas.py # REST/MCP request & response schemas
│ │ ├── service.py # KnowledgeService: index, status, reindex, semantic search
│ │ └── dependencies.py # get_knowledge_service() DI factory
│ ├── memory/
│ │ ├── models.py # ChatMessage, ConversationSession
│ │ ├── store.py # ConversationStore abstract interface + in-memory implementation
│ │ ├── session_manager.py # SessionManager: create/resume/expire/append/history
│ │ ├── context_builder.py # Builds conversation + retrieval context strings for prompts
│ │ ├── schemas.py # REST/MCP request & response schemas
│ │ └── dependencies.py # get_session_manager() DI factory
│ ├── rag/
│ │ ├── schemas.py # ChatRequest/ChatResponse/ChatSource
│ │ ├── service.py # RAGService: chat, resume_chat, career_chat, repository_chat,
│ │ │ # project_chat, and their streaming (chat_stream) counterpart
│ │ └── dependencies.py # get_rag_service() DI factory
│ ├── orchestration/
│ │ ├── schemas.py # TailorApplicationRequest/Result
│ │ ├── service.py # OrchestrationService: the tailored-application pipeline
│ │ └── dependencies.py # get_orchestration_service() DI factory
│ ├── services/
│ │ └── base_service.py # BaseService: shared scaffolding (logger) for every service
│ └── schemas/
│ └── common.py # Shared response schemas (HealthResponse, VersionResponse)
├── tests/ # pytest suite
├── Dockerfile
├── docker-compose.yml # Named volumes for uploads/ and data/chroma/ persistence
├── requirements.txt
├── pyproject.toml # ruff, mypy, pytest configuration
├── .env.example
└── .github/workflows/ci.ymlInstallation
Requires Python 3.12+.
git clone <repository-url> career-mcp-server
cd career-mcp-server
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
cp .env.example .envEnvironment Variables
All configuration is read from environment variables (or a local .env file). See
.env.example for defaults.
Variable | Default | Description |
|
| Application name |
|
| Application version |
|
| Bind host for the HTTP server |
|
| Bind port for the HTTP server |
|
| Minimum log level ( |
|
| Enables verbose diagnostics |
|
|
|
|
| Comma-separated list of allowed CORS origins |
| (none) | GitHub Personal Access Token (see Authentication) |
|
| Base URL for the GitHub REST API |
|
| Outbound HTTP request timeout, in seconds |
|
| TTL, in seconds, for cached GitHub data ( |
| (none) | OpenAI API key (required for AI-powered resume/career features) |
|
| Which |
|
| Model name requested from the configured provider |
|
| Maximum tokens generated per AI completion |
|
| Sampling temperature for AI completions |
|
| Local directory where uploaded resumes are stored |
|
| Maximum allowed resume upload size, in megabytes |
| (none) | Anthropic API key (required when |
| (none) | Google Gemini API key (required when |
| (none) | OpenRouter API key (required when |
| (none) | Azure OpenAI resource endpoint URL (required when |
| (none) | Azure OpenAI API key (required when |
|
| Azure OpenAI REST API version |
|
| Base URL of a running Ollama server (used by |
|
| Which provider generates vector embeddings: |
|
| Which vector database backs semantic search ( |
|
| Local filesystem path for the ChromaDB persistent store |
|
| Conversation session time-to-live, in seconds, before expiration |
|
| Maximum number of retrieved chunks included as RAG context per chat turn |
Running Locally
uvicorn app.main:app --reloadSwagger UI: http://localhost:8000/docs
ReDoc: http://localhost:8000/redoc
OpenAPI schema: http://localhost:8000/openapi.json
Health check: http://localhost:8000/health
Version: http://localhost:8000/version
Docker
docker compose up --buildThis builds a non-root, slim production image and starts the API on the port configured by
PORT (default 8000). Two named volumes persist across restarts: uploads (stored resume
files) and chroma_data (the ChromaDB persistent vector store, mounted at /app/data/chroma).
Running the MCP Server
The MCP server is built in app/mcp/server.py and initialized automatically when app.main is
imported (e.g. on application startup, it's attached to app.state.mcp_server). It registers 52
tools, 7 resources, and 8 prompts in total:
ping— the Phase 1 demonstration tool, proving MCP wiring works end-to-end.22 GitHub tools from
app/mcp/tools/github_tools.py.13 resume tools + 4 career tools from
app/mcp/tools/resume_tools.pyandcareer_tools.py.4 RAG chat tools + 4 knowledge-base tools + 3 session tools + 1 orchestration tool from
app/mcp/tools/{rag,knowledge,session,orchestration}_tools.py(see Available MCP Tools below).7 MCP Resources (
app/mcp/resources.py) and 8 MCP Prompts (app/mcp/prompts.py) — two MCP primitives distinct from tools; see MCP Resources and MCP Prompts below.
Each integration owns a register(registry) function in its own app/mcp/tools/* module (or
register(server) for resources/prompts, since those attach directly to the server rather than
the shared ToolRegistry); app/mcp/server.py just calls each of them once before building the
server, so adding a future integration never requires touching existing modules.
MCP tool arguments must be JSON-safe, so file uploads (upload_resume, replace_resume) take the
PDF's bytes as a base64-encoded string instead of a multipart upload. Streaming isn't exposed as an
MCP tool (tool results are single structured values, not token streams) — use POST /chat/stream
for a streaming experience instead.
GitHub Integration
Authentication
Generate a GitHub Personal Access Token (a fine-grained
token with read access to the repositories you want to query is sufficient) and set it in .env:
GITHUB_TOKEN=ghp_your_token_hereThe token is sent as a Bearer header on every request and is never logged. Without a token,
public endpoints (public repos, search, unauthenticated rate limit) still work — GitHub just
applies its much lower unauthenticated rate limit (60 requests/hour) and endpoints that require a
user context, like /github/profile, return a structured 401 AuthenticationException.
Available REST Endpoints
All endpoints are under /github and return typed JSON (never raw GitHub payloads):
Method | Path | Description |
GET |
| The authenticated user |
GET |
| The authenticated user's pinned repositories (GraphQL) |
GET |
| List the authenticated user's repositories |
GET |
| Search public repositories |
GET |
| Current GitHub API rate limit status |
GET |
| Repository details |
GET |
| Aggregated statistics |
GET |
| Language breakdown |
GET |
| Top contributors |
GET |
| Latest commits |
GET |
| Latest release |
GET |
| All releases |
GET |
| Decoded README |
GET |
| Repository topics |
GET |
| Repository license |
GET |
| Branches |
GET |
| Issues (excludes pull requests) |
GET |
| Pull requests |
GET |
| GitHub Actions workflows |
GET |
| Star count |
GET |
| Fork count |
GET |
| Watcher count |
DELETE |
| Manually invalidate the GitHub integration cache |
Example:
curl http://localhost:8000/github/repos/octocat/Hello-World/statsAvailable MCP Tools
The same functionality, one tool per REST endpoint above: github_profile,
pinned_repositories, list_repositories, search_repositories, rate_limit,
repository_details, repository_statistics, repository_languages,
repository_contributors, latest_commits, latest_release, repository_releases,
repository_readme, repository_topics, repository_license, repository_branches,
repository_issues, repository_pull_requests, repository_workflows, repository_stars,
repository_forks, repository_watchers.
Each tool's input/output schema is generated by the MCP SDK directly from its Python type hints and docstring — there is no separate schema to keep in sync.
Caching
GitHubService caches five read-heavy, slow-changing calls in an in-memory TTL cache (CACHE_TTL
seconds, default 300): the authenticated profile, the repository list, repository statistics,
language breakdowns, and releases. Everything else (commits, issues, PRs, branches, search, rate
limit, etc.) always hits GitHub directly. Call DELETE /github/cache to clear it manually.
Rate Limits
GitHub enforces 5,000 requests/hour for authenticated requests and 60/hour unauthenticated (10/min
for search). GET /github/rate-limit reports current usage. The client logs remaining quota on
every response and raises a structured RateLimitException (HTTP 429) the moment GitHub signals
either the primary or secondary rate limit has been hit — transient 5xx errors and network
failures are retried up to 3 times with exponential backoff before that.
Resume & AI Career Intelligence
Phase 3 adds resume upload/management, AI-powered resume analysis and scoring, resume-to-job
matching, and AI-generated rewrites, cover letters, and interview prep. All of it sits behind the
same provider-agnostic AIProvider abstraction — no business logic imports the OpenAI SDK
directly.
AI Provider Architecture
ResumeService / CareerService
│ depends only on the abstract interface
▼
AIProvider (app/ai/provider.py)
├── complete(prompt, system=..., temperature=..., max_tokens=...) -> str [abstract]
├── stream(...) -> AsyncIterator[str] [abstract]
└── complete_json(...) -> dict [concrete, built on complete()]
│
▼
BaseAIProvider (app/ai/base_provider.py) — settings storage, default resolution
│
▼
OpenAIProvider / AzureProvider / OpenRouterProvider / AnthropicProvider /
GeminiProvider / OllamaProviderapp/ai/factory.py exposes get_ai_provider(), a cached DI factory keyed by LLM_PROVIDER. As of
Phase 4, six providers are implemented — see
Supported Providers in the AI Platform section below. Adding a future
provider means writing one new class implementing complete()/stream() and registering it in the
factory's provider map — nothing in ResumeService, CareerService, the REST routes, or the MCP
tools changes.
Prompt Library
Every prompt used anywhere in the app lives in app/ai/prompts.py as a small function returning
plain text — nothing is hardcoded inline in a service. The module has no dependency on resume or
job-description models; callers pass already-serialized text (model.model_dump_json()), which
keeps app/ai reusable for any future feature, not just resumes.
Graceful Degradation
The AI provider is injected into ResumeService/CareerService as a factory callable, not a
pre-built instance. That means:
The app starts successfully with no
OPENAI_API_KEYset.GET /resume,GET /resume/{id},DELETE /resume/{id}work with no AI configured at all.POST /resume/uploadstill works with no AI configured — parsing degrades to deterministic, regex-based contact extraction (name/email/phone) with every AI-only field (skills, education, experience, ...) left as an empty default, rather than failing the upload.Everything genuinely AI-dependent (analyze, score, match, rewrite, cover letters, interview questions, career advice, ...) fails with a clean
502 ExternalServiceException—"OPENAI_API_KEY is not configured; AI-powered features are unavailable"— the moment it's actually called, not at startup.
Available REST Endpoints
Resume (/resume) — returns typed JSON, never raw AI text:
Method | Path | Description |
POST |
| Upload a PDF resume (multipart), parse, and store it |
GET |
| List stored resumes' metadata |
GET |
| Get a stored resume's metadata + parsed content |
PUT |
| Replace a stored resume's file, re-parsing it |
DELETE |
| Delete a stored resume |
POST |
| Full multi-part AI analysis (score, gaps, summaries, strengths/weaknesses) |
POST |
| Focused resume quality / ATS score |
POST |
| Compare a resume against a job description |
POST |
| AI-rewrite the full resume, summary, or experience section |
POST |
| Generate a tailored cover letter |
POST |
| Generate likely interview questions |
POST |
| Turn raw project notes into a polished description |
POST |
| Turn raw notes into quantified achievement bullets |
Career (/career) — general career intelligence, not necessarily tied to a stored resume:
Method | Path | Description |
POST |
| AI-powered career advice for a free-form question |
POST |
| Standalone summary from a stored resume or raw text |
POST |
| AI-rewrite a short personal bio |
POST |
| Extract structured fields from raw job posting text |
Example Requests & Responses
Upload a resume:
curl -X POST http://localhost:8000/resume/upload \
-F "file=@resume.pdf;type=application/pdf"{
"metadata": {
"id": "437afbe1254745e89c24197157a92a0c",
"original_filename": "resume.pdf",
"content_type": "application/pdf",
"size_bytes": 48213,
"uploaded_at": "2026-01-15T10:30:00Z",
"updated_at": "2026-01-15T10:30:00Z"
},
"parsed": {
"contact": {"name": "Jane Doe", "email": "jane@example.com", "phone": "+1-555-0100", "location": "Remote"},
"summary": "Backend engineer with 6 years building distributed systems...",
"skills": ["Python", "FastAPI", "PostgreSQL"],
"education": ["..."], "experience": ["..."], "projects": ["..."],
"certifications": [], "languages": [], "publications": [],
"raw_text": "..."
}
}Match a resume against a job description:
curl -X POST http://localhost:8000/resume/match \
-H "Content-Type: application/json" \
-d '{"resume_id": "437afbe1254745e89c24197157a92a0c", "job_description": "We need a backend engineer with FastAPI and PostgreSQL experience..."}'{
"overall_score": 88,
"skill_match_score": 90,
"experience_match_score": 85,
"keyword_coverage_score": 80,
"missing_skills": ["Kubernetes"],
"recommendations": ["Add a metric quantifying the API's scale"],
"ats_compatibility_score": 92
}Available MCP Tools
Resume (13): upload_resume, list_resumes, get_resume, replace_resume,
delete_resume, analyze_resume, resume_score, match_resume, rewrite_resume,
generate_cover_letter, interview_questions, project_descriptions, achievement_generator.
Career (4): career_advice, professional_summary, rewrite_bio, parse_job_description.
Each tool's input/output schema is generated by the MCP SDK directly from its Python type hints
and docstring. upload_resume and replace_resume take content_base64 (a base64-encoded PDF)
instead of a file upload, since MCP tool arguments must be JSON-safe.
Security
File type validation — only
application/pdfis accepted; anything else is rejected with a structured415 UnsupportedMediaTypeExceptionbefore any file processing happens.PDF signature check — uploaded bytes must start with the
%PDF-magic bytes, rejecting mislabeled files even if the declared content type says PDF.Size limits — uploads over
MAX_UPLOAD_SIZE_MBare rejected with413 PayloadTooLargeException.Path traversal prevention — storage is always keyed by a server-generated UUID (
{resume_id}.pdf), never by the user-supplied filename, so path traversal via a crafted filename is structurally impossible regardless of storage backend. The original filename is still sanitized (directory components stripped, unsafe characters rejected) before being stored as display metadata.No public file exposure — there is no static file route; the only way to retrieve a resume's content is through the authenticated-by-deployment
GET /resume/{resume_id}API, which returns parsed data, not the raw file.
AI Platform, RAG, Memory & Advanced MCP
Phase 4 turns the single-provider AI abstraction from Phase 3 into a full platform: six interchangeable LLM providers, a pluggable embeddings + vector store layer, a knowledge base built from indexed resumes and GitHub repositories, retrieval-augmented chat with conversation memory and streaming, a multi-step orchestration pipeline, and the MCP Resources and Prompts primitives alongside tools.
Supported Providers
Provider |
| Requires |
OpenAI |
|
|
Anthropic (Claude) |
|
|
Google Gemini |
|
|
Azure OpenAI |
|
|
OpenRouter |
|
|
Ollama (local models) |
|
|
All six implement the same AIProvider interface (complete(), complete_json(), stream(),
aclose()); OpenAIProvider, AzureProvider, and OpenRouterProvider share their wire-format
logic via ChatCompletionsMixin since all three speak the OpenAI Chat Completions API. Selecting a
provider is one environment variable (LLM_PROVIDER) — nothing in ResumeService, CareerService,
RAGService, or the REST/MCP layers changes.
Embeddings follow the identical pattern via EmbeddingProvider (app/ai/embeddings/):
Provider |
| Requires |
OpenAI |
|
|
Ollama |
|
|
Sentence Transformers (local) |
|
|
Vector Database & Document Chunking
Semantic search is backed by a VectorStore abstraction (app/vectorstore/store.py) with a
ChromaDB implementation (ChromaVectorStore, VECTOR_DB=chroma, the only backend implemented so
far — Pinecone/Qdrant/Weaviate/FAISS are future work behind the same interface). ChromaDB's
synchronous Python client is run through asyncio.to_thread() to stay consistent with the app's
async-first design; its persistent store lives at CHROMA_PATH (default data/chroma, gitignored
and given its own Docker volume — see Docker).
Documents are split for indexing by app/knowledge/chunking.py: a recursive splitter tries
paragraph breaks, then lines, then sentences, then words (falling back to a hard character cut only
for a single unbroken run longer than chunk_size), then greedily re-merges adjacent pieces up to
chunk_size characters with chunk_overlap characters carried from the end of one chunk into the
start of the next, so retrieval doesn't lose context at chunk boundaries. Every chunk carries
metadata (source_type, source_id, section, ...) so search results can be filtered and traced
back to their origin.
Knowledge Base
KnowledgeService (app/knowledge/service.py) indexes two kinds of source into two ChromaDB
collections:
Resume (
resumescollection) —app/knowledge/resume_indexer.pybuilds section-tagged chunks (summary, skills, education, experience, project, certification) from a stored resume.GitHub (
githubcollection) —app/knowledge/github_indexer.pybuilds chunks from a repository's README, languages, topics, releases, and recent commits.
Search targets (resume, projects, skills, repositories, documents) map to one or both
collections, optionally filtered by chunk metadata (e.g. projects searches the resumes
collection filtered to section=project); documents searches both collections. reindex_all()
clears both collections and re-indexes every stored resume (GitHub repositories aren't
automatically tracked, so they're re-indexed on demand via POST /knowledge/index).
Retrieval-Augmented Generation & Conversation Memory
RAGService (app/rag/service.py) is the single implementation behind every chat variant:
chat() takes a mode (general / resume / project / career / repository) that selects
which knowledge-base target to search for context; resume_chat(), project_chat(),
career_chat(), and repository_chat() are thin wrappers with the mode pre-selected. Each turn:
sanitizes the message (app/ai/sanitize.py), resolves or creates a conversation session, retrieves
relevant chunks via KnowledgeService.semantic_search(), builds a prompt from the retrieved context
plus prior conversation history, calls the configured AIProvider, and appends both the user
message and the reply to session history.
Conversation memory lives in app/memory/: SessionManager handles create/resume/delete and
expiration (SESSION_TTL seconds of inactivity); ConversationStore is an abstract interface with
an in-memory implementation; context_builder.py renders a session's summary + recent messages
into the prompt.
Streaming
POST /chat/stream returns a text/event-stream Server-Sent Events response: the first event is
event: session carrying the (possibly newly created) session ID, so a client can learn it before
any reply text arrives, followed by event: message chunks as the LLM streams its reply, ending
with event: done. Streaming isn't exposed as an MCP tool (see
Running the MCP Server).
MCP Resources
Resources are a distinct MCP primitive from tools: clients discover and read them by URI rather
than invoking them with arguments. Registered in app/mcp/resources.py:
URI | Description |
| Metadata for every stored resume |
| A specific stored resume's metadata and full parsed content (templated) |
| Every stored resume plus pinned GitHub repositories |
| The authenticated user's GitHub repositories |
| Education + experience aggregated across stored resumes |
| Unique skills aggregated across all stored resumes |
| Projects aggregated across all stored resumes |
| What's currently indexed in the knowledge base |
MCP Prompts
Prompts are reusable, discoverable templates an MCP client fetches and sends to its own LLM (this
server doesn't call an AI provider on the client's behalf for these). Registered in
app/mcp/prompts.py, with wording centralized in app/ai/prompts.py: resume_review,
interview_preparation, career_coach, github_reviewer, project_explainer,
portfolio_reviewer, cover_letter_generator, prompt_engineer.
Tool Orchestration
OrchestrationService.tailor_application() (app/orchestration/service.py) chains existing
services into one pipeline rather than duplicating logic: fetch a stored resume for its skills →
parse the job description (CareerService.parse_job_description()) → retrieve relevant indexed
projects and GitHub context (KnowledgeService) → generate a tailored resume, cover letter, and
interview questions (ResumeService) — returned as one combined, structured result.
Semantic Search
KnowledgeService.semantic_search() and its named wrappers (search_resume(),
search_projects(), search_skills(), search_repositories(), search_documents()) embed the
query with the configured EmbeddingProvider, query the relevant ChromaDB collection(s), and
return results ranked by a cosine-similarity approximation (1 - distance, clamped to [0, 1]),
optionally filtered by a minimum threshold.
Observability
app/core/metrics.py provides dependency-free, thread-safe counters and histograms (deliberately
not a Prometheus/OpenTelemetry integration — swapping one in later means replacing this module's
internals without touching any call site): HTTP request counts/duration, LLM request duration and
token usage, cache hits/misses, vector search duration, and session created/deleted counts. Exposed
via GET /metrics.
Security
Prompt-injection mitigation —
app/ai/sanitize.pystrips control characters and caps input length (sanitize_user_input()), and can fence untrusted retrieved content with explicit "not instructions" delimiters (fence_untrusted_content()). Documented as practical, not foolproof, mitigations.No internal prompt exposure — prompt templates are never returned to clients; only their rendered output (chat replies) or, for MCP Prompts, the template itself when a client explicitly requests that named prompt (by design, since that's the MCP Prompts contract).
API keys never logged — every provider constructor validates key presence and raises a structured
ExternalServiceExceptionwithout ever logging the key value.
Available REST Endpoints
Method | Path | Description |
POST |
| Retrieval-augmented chat, one complete response |
POST |
| Same, streamed via Server-Sent Events |
POST |
| Semantic search across the knowledge base (alias for |
POST |
| Create a new conversation session |
GET |
| List active (non-expired) conversation sessions |
GET |
| Get a session's full message history |
DELETE |
| Delete a conversation session |
POST |
| Index a resume or GitHub repository into the knowledge base |
GET |
| Document counts per knowledge-base collection |
POST |
| Clear and re-index every stored resume |
POST |
| Semantic search across the knowledge base |
GET |
| In-process observability metrics snapshot |
POST |
| Run the full resume-to-tailored-application pipeline |
Available MCP Tools
RAG chat (4): chat, resume_chat, career_chat, repository_chat.
Knowledge base (4): semantic_search, knowledge_status, reindex_documents,
generate_embeddings.
Sessions (3): create_session, delete_session, conversation_history.
Orchestration (1): tailor_application.
Example Workflow
Index a resume, then ask a scoped question grounded in it:
curl -X POST http://localhost:8000/knowledge/index \
-H "Content-Type: application/json" \
-d '{"source_type": "resume", "resume_id": "437afbe1254745e89c24197157a92a0c"}'
curl -X POST http://localhost:8000/chat \
-H "Content-Type: application/json" \
-d '{"message": "What are my strongest technical skills?", "mode": "resume"}'{
"session_id": "8b200fba8fa24aab923ae7c025afe05e",
"reply": "Based on your resume, your strongest skills are Python, FastAPI, and PostgreSQL...",
"sources": [{"id": "...", "text": "...", "score": 0.87}]
}Run the full tailored-application pipeline in one call:
curl -X POST http://localhost:8000/orchestration/tailor-application \
-H "Content-Type: application/json" \
-d '{"resume_id": "437afbe1254745e89c24197157a92a0c", "job_description": "We need a backend engineer with FastAPI and PostgreSQL experience..."}'Testing
pytest --cov=app --cov-report=term-missingTests cover:
The
/healthand/versionendpointsConfiguration loading, validation, and caching
The MCP server's initialization and the
pingtoolApplication startup/shutdown lifecycle
The GitHub HTTP client: auth headers, 401/403/404/429/5xx mapping, retry-with-backoff, pagination — all against
httpx.MockTransport, no live network callsGitHubService: every method, JSON-to-model mapping, caching behavior, input validationEvery
/github/*REST endpoint, including error-envelope translation, via dependency overridesEvery GitHub MCP tool: registration, delegation, and typed-exception propagation
The AI provider abstraction:
complete_jsonJSON/code-fence parsing,OpenAIProvider(with the OpenAI SDK client mocked), and theget_ai_provider()factory's provider selection/error pathsResume upload validation, local storage (save/get/list/delete/replace), PDF text extraction against a real (hand-built, dependency-free) PDF, and AI-powered field extraction with graceful degradation when the AI provider is unavailable or fails
ResumeServiceandCareerService: every method, against in-memory storage and AI provider fakes — no filesystem I/O beyond atmp_path, no real AI callsEvery
/resume/*and/career/*REST endpoint and every resume/career MCP tool: registration, delegation, file upload handling, and error-envelope translationAll 6 LLM providers (Anthropic, Gemini, Ollama, Azure, OpenRouter, plus the existing OpenAI provider), each with its SDK/HTTP client fully mocked, and the prompt-injection sanitizer
All 3 embedding providers, and
ChromaVectorStoreexercised against a real ChromaDB instance backed bytmp_path(deliberately not mocked — this is what surfaced a real empty-metadata rejection bug in the installed ChromaDB version)Document chunking: separator fallback, overlap carry-over, and the oversized-chunk edge case found when
chunk_overlapis large relative tochunk_sizeKnowledgeService: indexing, status, reindexing, and every semantic-search target, against in-memory vector-store and embedding-provider fakes typed against the real Phase 2/3 modelsConversation memory: the in-memory store,
SessionManager(create/resume/expire/append/history), and the context builderRAGServiceandOrchestrationService: every chat mode, streaming, session handling, and the full tailored-application pipeline, against fakes — no real AI or vector-store callsMetricsRegistry: counters, histograms, thetimer()context manager, and snapshot orderingEvery new
/chat,/sessions,/knowledge/*,/search,/metrics, and/orchestration/*REST endpoint via dependency overridesEvery new RAG/knowledge/session/orchestration MCP tool: registration, delegation, and typed-exception propagation
Every MCP Resource (list + read, via the live MCP server) and every MCP Prompt (list + argument rendering, via the live MCP server)
No test makes a real GitHub API call or a real AI API call — every external client is fully swappable via dependency injection.
CI/CD
GitHub Actions (.github/workflows/ci.yml) runs on every push and
pull request to main:
Install dependencies (pip cache enabled)
ruff check .— lintmypy app— type-check (strict mode)pytest --cov=app --cov-fail-under=90— test, failing the build if coverage regresses below 90%; the coverage report (coverage.xml) is uploaded as a workflow artifactBuild the production Docker image via
docker/build-push-action, with GitHub Actions layer caching (cache-from/cache-to: type=gha) so unchanged layers aren't rebuilt on every run
The pipeline fails on any lint, type, test, coverage-threshold, or Docker build error.
Local pre-commit hooks (optional)
pip install pre-commit
pre-commit installRuns ruff check --fix and mypy app automatically before each commit, plus standard hygiene
checks (trailing whitespace, large files, merge conflict markers). Configured in
.pre-commit-config.yaml.
Roadmap
Phase 1 — Project foundation: FastAPI app, MCP server bootstrap, configuration, logging, middleware, structured error handling, Docker, CI.
Phase 2 — GitHub integration: async client, typed models, caching service layer, 22 REST endpoints, and 22 matching MCP tools.
Phase 3 — AI career intelligence & resume engine: provider-agnostic AI abstraction, PDF resume upload/parsing/storage, AI-powered analysis/scoring/matching/rewriting, 17 REST endpoints, and 17 matching MCP tools.
Phase 4 — AI platform, RAG, memory & advanced MCP: 6 LLM providers, 3 embedding providers, a ChromaDB-backed vector store, resume + GitHub knowledge-base indexing, retrieval-augmented chat with conversation memory and SSE streaming, a tailored-application orchestration pipeline, MCP Resources and Prompts, observability metrics, 13 new REST endpoints, and 12 matching MCP tools.
v1.0.0 (this repository) — the first stable release: Phases 1–4 combined, audited, and hardened for production use. See CHANGELOG.md and RELEASE_NOTES_v1.0.0.md.
Phase 5 — LinkedIn integration.
Phase 6 — Further AI-powered career insights and recommendations.
Known Limitations
Conversation memory is in-memory only.
ConversationStore(app/memory/store.py) has an in-memory implementation only, so chat sessions do not survive an application restart. The interface is designed to support a persistent backend (Redis, a database) later without changingSessionManageror any caller.Vector store: ChromaDB only.
VectorStore(app/vectorstore/store.py) is designed as a swappable interface, but Pinecone/Qdrant/Weaviate/FAISS implementations don't exist yet.No built-in authentication. Neither the REST API nor the MCP server enforces authentication/authorization — see SECURITY.md for deployment guidance.
GitHub knowledge-base indexing is manual. Unlike resumes (which
reindex_all()re-indexes automatically), GitHub repositories must be indexed explicitly viaPOST /knowledge/index(or thesemantic_search-adjacent MCP tools) — they aren't tracked/discovered automatically.Cosine similarity scores are an approximation (
1 - distance, clamped to[0, 1]), not a formally calibrated probability — treatthresholdfiltering as a useful heuristic, not an exact guarantee.
FAQ
Do I need every LLM provider's API key to run this?
No. LLM_PROVIDER selects exactly one provider; only that provider's key is required. The app
also starts successfully with no AI key configured at all — AI-dependent endpoints return a
clean 502 ExternalServiceException only when actually called (see
Graceful Degradation).
Can I use a fully local setup (no cloud AI calls)?
Yes — set LLM_PROVIDER=ollama and EMBEDDING_PROVIDER=ollama (or sentence_transformers) with
a local Ollama server running; no API key or outbound AI traffic is
required. GitHub calls still go to api.github.com unless you don't use the GitHub integration.
Why MCP and REST — why not just one? Different clients need different transports: a human or a traditional frontend calls REST; an LLM-based agent (Claude Desktop, an MCP-aware IDE, etc.) calls MCP tools directly. Building both on top of identical service classes means neither transport is a second-class citizen or a maintenance burden.
Is this production-ready?
As of v1.0.0, yes, within the scope described in Known Limitations above
and SECURITY.md — it's fully tested, typed, containerized, and has run through a
full production-readiness audit. Evaluate the specific limitations against your own deployment
requirements first.
Contributing
See CONTRIBUTING.md for the full guide. In short:
Create a feature branch.
Make your changes with type hints, docstrings, and tests.
Run
ruff check .,mypy app, andpytestlocally (or install the pre-commit hooks).Open a pull request; CI must pass before merge.
This project follows the Code of Conduct. Found a security issue? See SECURITY.md instead of opening a public issue.
License
Distributed under the MIT License.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- Alicense-qualityDmaintenanceA high-performance FastAPI server supporting Model Context Protocol (MCP) for seamless integration with Large Language Models, featuring REST, GraphQL, and WebSocket APIs, along with real-time monitoring and vector search capabilities.Last updated8MIT
- Alicense-quality-maintenanceA standardized MCP server implementation that provides AI models with dynamic tool discovery, execution, and context management capabilities. Built with FastAPI, it offers a modular architecture for easily adding new tools and managing AI application interactions through the Model Context Protocol.Last updated13
- AlicenseBqualityDmaintenanceA fast, secure, and LLM-friendly Model Context Protocol (MCP) server that scrapes job listings from major platforms (LinkedIn, Indeed, Google) and converts them into structured Markdown format.Last updated11MIT
- AlicenseAqualityDmaintenanceA Model Context Protocol (MCP) server that exposes a professional profile — certifications, industry articles, open source contributions, and live GitHub activity — as a queryable API for AI agents.Last updated711MIT
Related MCP Connectors
A comprehensive Model Context Protocol (MCP) server that enables AI assistants to interact with yo…
A Model Context Protocol (MCP) application for automated GitHub PR analysis and issue management.…
Hosted MCP server for LLM cost estimation, model comparison, and budget-aware routing.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/mubashirnaeemj/career-mcp-server'
If you have feedback or need assistance with the MCP directory API, please join our Discord server