Skip to main content
Glama

career-mcp-server

CI Version Python FastAPI License

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/utils

Every 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 BaseService base class so domain code stays consistent across integrations.

  • Provider-agnostic AI — every AI-backed service depends on the abstract AIProvider interface (app/ai/provider.py), never on a specific SDK directly. Swapping LLM_PROVIDER swaps 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 to ResumeService, CareerService, KnowledgeService, RAGService, and OrchestrationService.

  • Structured errors — every handled failure, from either transport, returns the same JSON error envelope ({"success": false, "error": {...}}) or the equivalent MCP is_error result.

  • 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_KEY configured; 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

openai, anthropic, google-genai (+ plain httpx for Ollama)

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.yml

Installation

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 .env

Environment Variables

All configuration is read from environment variables (or a local .env file). See .env.example for defaults.

Variable

Default

Description

APP_NAME

career-mcp-server

Application name

APP_VERSION

1.0.0

Application version

HOST

0.0.0.0

Bind host for the HTTP server

PORT

8000

Bind port for the HTTP server

LOG_LEVEL

INFO

Minimum log level (DEBUG, INFO, ...)

DEBUG

false

Enables verbose diagnostics

ENVIRONMENT

development

development / staging / production / test

CORS_ORIGINS

*

Comma-separated list of allowed CORS origins

GITHUB_TOKEN

(none)

GitHub Personal Access Token (see Authentication)

GITHUB_API

https://api.github.com

Base URL for the GitHub REST API

REQUEST_TIMEOUT

30

Outbound HTTP request timeout, in seconds

CACHE_TTL

300

TTL, in seconds, for cached GitHub data (0 effectively disables caching)

OPENAI_API_KEY

(none)

OpenAI API key (required for AI-powered resume/career features)

LLM_PROVIDER

openai

Which AIProvider implementation backs AI features

LLM_MODEL

gpt-5

Model name requested from the configured provider

MAX_TOKENS

4000

Maximum tokens generated per AI completion

TEMPERATURE

0.3

Sampling temperature for AI completions

UPLOAD_DIRECTORY

uploads

Local directory where uploaded resumes are stored

MAX_UPLOAD_SIZE_MB

10

Maximum allowed resume upload size, in megabytes

ANTHROPIC_API_KEY

(none)

Anthropic API key (required when LLM_PROVIDER=anthropic)

GEMINI_API_KEY

(none)

Google Gemini API key (required when LLM_PROVIDER=gemini)

OPENROUTER_API_KEY

(none)

OpenRouter API key (required when LLM_PROVIDER=openrouter)

AZURE_OPENAI_ENDPOINT

(none)

Azure OpenAI resource endpoint URL (required when LLM_PROVIDER=azure)

AZURE_OPENAI_KEY

(none)

Azure OpenAI API key (required when LLM_PROVIDER=azure)

AZURE_OPENAI_API_VERSION

2024-08-01-preview

Azure OpenAI REST API version

OLLAMA_BASE_URL

http://localhost:11434

Base URL of a running Ollama server (used by LLM_PROVIDER=ollama and EMBEDDING_PROVIDER=ollama)

EMBEDDING_PROVIDER

openai

Which provider generates vector embeddings: openai / ollama / sentence_transformers

VECTOR_DB

chroma

Which vector database backs semantic search (chroma only, for now)

CHROMA_PATH

data/chroma

Local filesystem path for the ChromaDB persistent store

SESSION_TTL

3600

Conversation session time-to-live, in seconds, before expiration

MAX_CONTEXT_CHUNKS

10

Maximum number of retrieved chunks included as RAG context per chat turn

Running Locally

uvicorn app.main:app --reload

Docker

docker compose up --build

This 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.py and career_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_here

The 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

/github/profile

The authenticated user

GET

/github/pinned

The authenticated user's pinned repositories (GraphQL)

GET

/github/repos

List the authenticated user's repositories

GET

/github/search?q=

Search public repositories

GET

/github/rate-limit

Current GitHub API rate limit status

GET

/github/repos/{owner}/{repo}

Repository details

GET

/github/repos/{owner}/{repo}/stats

Aggregated statistics

GET

/github/repos/{owner}/{repo}/languages

Language breakdown

GET

/github/repos/{owner}/{repo}/contributors

Top contributors

GET

/github/repos/{owner}/{repo}/commits

Latest commits

GET

/github/repos/{owner}/{repo}/release

Latest release

GET

/github/repos/{owner}/{repo}/releases

All releases

GET

/github/repos/{owner}/{repo}/readme

Decoded README

GET

/github/repos/{owner}/{repo}/topics

Repository topics

GET

/github/repos/{owner}/{repo}/license

Repository license

GET

/github/repos/{owner}/{repo}/branches

Branches

GET

/github/repos/{owner}/{repo}/issues

Issues (excludes pull requests)

GET

/github/repos/{owner}/{repo}/pulls

Pull requests

GET

/github/repos/{owner}/{repo}/workflows

GitHub Actions workflows

GET

/github/repos/{owner}/{repo}/stars

Star count

GET

/github/repos/{owner}/{repo}/forks

Fork count

GET

/github/repos/{owner}/{repo}/watchers

Watcher count

DELETE

/github/cache

Manually invalidate the GitHub integration cache

Example:

curl http://localhost:8000/github/repos/octocat/Hello-World/stats

Available 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 / OllamaProvider

app/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_KEY set.

  • GET /resume, GET /resume/{id}, DELETE /resume/{id} work with no AI configured at all.

  • POST /resume/upload still 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

/resume/upload

Upload a PDF resume (multipart), parse, and store it

GET

/resume

List stored resumes' metadata

GET

/resume/{resume_id}

Get a stored resume's metadata + parsed content

PUT

/resume/{resume_id}

Replace a stored resume's file, re-parsing it

DELETE

/resume/{resume_id}

Delete a stored resume

POST

/resume/analyze

Full multi-part AI analysis (score, gaps, summaries, strengths/weaknesses)

POST

/resume/score

Focused resume quality / ATS score

POST

/resume/match

Compare a resume against a job description

POST

/resume/rewrite

AI-rewrite the full resume, summary, or experience section

POST

/resume/cover-letter

Generate a tailored cover letter

POST

/resume/interview

Generate likely interview questions

POST

/resume/project-descriptions

Turn raw project notes into a polished description

POST

/resume/achievements

Turn raw notes into quantified achievement bullets

Career (/career) — general career intelligence, not necessarily tied to a stored resume:

Method

Path

Description

POST

/career/advice

AI-powered career advice for a free-form question

POST

/career/professional-summary

Standalone summary from a stored resume or raw text

POST

/career/rewrite-bio

AI-rewrite a short personal bio

POST

/career/parse-job-description

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/pdf is accepted; anything else is rejected with a structured 415 UnsupportedMediaTypeException before 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_MB are rejected with 413 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

LLM_PROVIDER value

Requires

OpenAI

openai

OPENAI_API_KEY

Anthropic (Claude)

anthropic

ANTHROPIC_API_KEY

Google Gemini

gemini

GEMINI_API_KEY

Azure OpenAI

azure

AZURE_OPENAI_ENDPOINT, AZURE_OPENAI_KEY

OpenRouter

openrouter

OPENROUTER_API_KEY

Ollama (local models)

ollama

OLLAMA_BASE_URL running (no API key)

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

EMBEDDING_PROVIDER value

Requires

OpenAI

openai

OPENAI_API_KEY

Ollama

ollama

OLLAMA_BASE_URL running

Sentence Transformers (local)

sentence_transformers

pip install sentence-transformers (optional, ~2GB with torch — not installed by default)

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 (resumes collection) — app/knowledge/resume_indexer.py builds section-tagged chunks (summary, skills, education, experience, project, certification) from a stored resume.

  • GitHub (github collection) — app/knowledge/github_indexer.py builds 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

resume://list

Metadata for every stored resume

resume://{resume_id}

A specific stored resume's metadata and full parsed content (templated)

portfolio://summary

Every stored resume plus pinned GitHub repositories

github://repositories

The authenticated user's GitHub repositories

career://history

Education + experience aggregated across stored resumes

skills://all

Unique skills aggregated across all stored resumes

projects://all

Projects aggregated across all stored resumes

documents://index

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.

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 mitigationapp/ai/sanitize.py strips 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 ExternalServiceException without ever logging the key value.

Available REST Endpoints

Method

Path

Description

POST

/chat

Retrieval-augmented chat, one complete response

POST

/chat/stream

Same, streamed via Server-Sent Events

POST

/search

Semantic search across the knowledge base (alias for /knowledge/search)

POST

/sessions

Create a new conversation session

GET

/sessions

List active (non-expired) conversation sessions

GET

/sessions/{session_id}

Get a session's full message history

DELETE

/sessions/{session_id}

Delete a conversation session

POST

/knowledge/index

Index a resume or GitHub repository into the knowledge base

GET

/knowledge/status

Document counts per knowledge-base collection

POST

/knowledge/reindex

Clear and re-index every stored resume

POST

/knowledge/search

Semantic search across the knowledge base

GET

/metrics

In-process observability metrics snapshot

POST

/orchestration/tailor-application

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-missing

Tests cover:

  • The /health and /version endpoints

  • Configuration loading, validation, and caching

  • The MCP server's initialization and the ping tool

  • Application 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 calls

  • GitHubService: every method, JSON-to-model mapping, caching behavior, input validation

  • Every /github/* REST endpoint, including error-envelope translation, via dependency overrides

  • Every GitHub MCP tool: registration, delegation, and typed-exception propagation

  • The AI provider abstraction: complete_json JSON/code-fence parsing, OpenAIProvider (with the OpenAI SDK client mocked), and the get_ai_provider() factory's provider selection/error paths

  • Resume 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

  • ResumeService and CareerService: every method, against in-memory storage and AI provider fakes — no filesystem I/O beyond a tmp_path, no real AI calls

  • Every /resume/* and /career/* REST endpoint and every resume/career MCP tool: registration, delegation, file upload handling, and error-envelope translation

  • All 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 ChromaVectorStore exercised against a real ChromaDB instance backed by tmp_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_overlap is large relative to chunk_size

  • KnowledgeService: indexing, status, reindexing, and every semantic-search target, against in-memory vector-store and embedding-provider fakes typed against the real Phase 2/3 models

  • Conversation memory: the in-memory store, SessionManager (create/resume/expire/append/history), and the context builder

  • RAGService and OrchestrationService: every chat mode, streaming, session handling, and the full tailored-application pipeline, against fakes — no real AI or vector-store calls

  • MetricsRegistry: counters, histograms, the timer() context manager, and snapshot ordering

  • Every new /chat, /sessions, /knowledge/*, /search, /metrics, and /orchestration/* REST endpoint via dependency overrides

  • Every 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:

  1. Install dependencies (pip cache enabled)

  2. ruff check . — lint

  3. mypy app — type-check (strict mode)

  4. 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 artifact

  5. Build 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 install

Runs 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 changing SessionManager or 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 via POST /knowledge/index (or the semantic_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 — treat threshold filtering 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:

  1. Create a feature branch.

  2. Make your changes with type hints, docstrings, and tests.

  3. Run ruff check ., mypy app, and pytest locally (or install the pre-commit hooks).

  4. 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.

A
license - permissive license
-
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

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

  • A
    license
    -
    quality
    D
    maintenance
    A 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 updated
    8
    MIT
  • A
    license
    -
    quality
    -
    maintenance
    A 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 updated
    13
  • A
    license
    B
    quality
    D
    maintenance
    A 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 updated
    1
    1
    MIT
  • A
    license
    A
    quality
    D
    maintenance
    A 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 updated
    7
    11
    MIT

View all related MCP servers

Related MCP Connectors

View all MCP Connectors

Latest Blog Posts

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