Employee Management MCP Server
Enables employee management operations including listing, searching, creating, updating, and deactivating employees, as well as generating organizational summaries, using a synthetic JSON dataset.
README
Employee Management MCP Server (EMP-MCP-2026)
An MCP server exposing 6 employee-management tools over stdio, backed by a synthetic JSON employee dataset.
Tools
| Tool | Purpose |
|---|---|
list_employees |
Filter/search employees (department, status, location, free-text query). |
get_employee |
Fetch one employee's full profile by employee_id. |
get_org_summary |
Headcount aggregation by department / status / location. |
create_employee |
Onboard a new hire. |
update_employee |
Partial update of an existing employee's mutable fields. |
deactivate_employee |
Soft-delete (sets status to inactive; record stays retrievable). |
Setup
Requires Python 3.10+.
python -m venv .venv
.venv\Scripts\activate # Windows
pip install -e .
Copy .env.example to .env and adjust if needed (no secrets live in either file —
only paths and mode flags). Note: the server does not auto-load .env — nothing
in config.py calls load_dotenv(). python-dotenv is listed as a dependency but
isn't wired in. .env is documentation of what to set; to make it take effect you
either export those variables in your shell before running, set
them directly in the env block of your MCP host's server config (the common case —
see "Host configuration" below), or run dotenv run -- python -m employee_mcp.server
yourself if you want the file loaded.
Running the server
python -m employee_mcp.server
On first run, if EMP_MCP_DATA_PATH doesn't exist yet, the server copies the committed
seed (data/employees.seed.json, 14 synthetic employees across all 5 departments) to
that path and treats it as the mutable working dataset. Delete that file to reset to a
clean seed on the next run.
Windows note: make sure python resolves to the interpreter with mcp installed
(or point a host's command at the venv's absolute python.exe). Prefer an absolute
EMP_MCP_DATA_PATH when a host launches this as a subprocess — a relative path combined
with a host-chosen working directory is the most common cause of "connected but no
tools" / "connection closed" on Windows. In this codebase, relative EMP_MCP_DATA_PATH
values are resolved against the repo root (not the process's cwd) specifically to avoid
that trap, but an absolute path is still the most robust choice for host configs.
Data storage
Employees live in a single JSON array file (data/employees.json), loaded in full at
startup into an employee_id -> Employee dict (store.py) for O(1) lookups. Every
write (create_employee / update_employee / deactivate_employee) re-serializes
the whole in-memory set and persists it atomically — write to a .tmp file, then
os.replace() over the target — so a crash or kill mid-write can't leave a
half-written, corrupt data file.
JSON (rather than a real database) is a deliberate choice for this project's scale: the seed is 14 employees, and this design targets small in-memory datasets on the order of hundreds to low thousands of records, not enterprise HRIS volumes. At that size, loading the entire file into memory on every server start has no meaningful cost, and JSON buys a human-readable, git-diffable store with zero schema migrations, no separate database process to run, and no driver to install — appropriate for a synthetic-data assignment/demo, not a claim that this approach would scale past that ceiling (see "Known limitations" for what would need to change if it did — e.g. real pagination, an actual DB engine, and finer-grained locking than the current "rewrite the whole file" strategy allows for concurrent writers).
Configuration (environment variables)
| Env var | Default | Meaning |
|---|---|---|
EMP_MCP_DATA_PATH |
./data/employees.json |
Mutable working data file. |
EMP_MCP_READ_ONLY |
false |
When true, all write tools refuse with a READ_ONLY error and make no changes. |
EMP_MCP_DEBUG |
false |
When true, unexpected-error messages include a traceback. Keep false in normal/demo use. |
EMP_MCP_AUDIT_PATH |
./logs/audit.log |
Path to the audit log file (one JSON line per tool call). |
No secrets are configured anywhere — these are mode flags and paths only.
Host configuration
{
"mcpServers": {
"employee-mcp": {
"command": "<ABSOLUTE-PATH-TO-YOUR-python.exe>",
"args": ["-m", "employee_mcp.server"],
"env": {
"PYTHONPATH": "<ABSOLUTE-PATH-TO-THIS-REPO-ROOT>",
"EMP_MCP_DATA_PATH": "<ABSOLUTE-PATH-TO-THIS-REPO-ROOT>\\data\\employees.json",
"EMP_MCP_AUDIT_PATH": "<ABSOLUTE-PATH-TO-THIS-REPO-ROOT>\\logs\\audit.log",
"EMP_MCP_READ_ONLY": "false",
"EMP_MCP_DEBUG": "false"
}
}
}
}
Fill in the two placeholders for your own machine (every path must be absolute — a relative path combined with a host-chosen working directory is the most common cause of "connected but no tools" / "connection closed" on Windows):
<ABSOLUTE-PATH-TO-YOUR-python.exe>— the interpreter you installed this project into. If you used a venv (python -m venv .venvper Setup above), activate it and run(Get-Command python).Sourcein PowerShell, then paste that path, e.g.C:\Users\you\...\Employee_management_MCP_server\.venv\Scripts\python.exe.<ABSOLUTE-PATH-TO-THIS-REPO-ROOT>— the folder containing this README on your machine, e.g.C:\Users\you\Documents\Employee_management_MCP_server. It's used four times above: once forPYTHONPATH, and as the prefix for both data-file paths — replace all of them consistently.
PYTHONPATH matters specifically when command points at a bare interpreter that
this project was never pip install -e .'d into — without it, python -m employee_mcp.server can't locate the employee_mcp package and the host will show
the server as failed/disconnected. If command points at a venv's python.exe where
you already ran pip install -e ., PYTHONPATH is unnecessary — the editable install
makes the package importable from anywhere — but it doesn't hurt to leave it set.
Debugging with MCP Inspector
npx @modelcontextprotocol/inspector python -m employee_mcp.server
This opens a local web UI (http://localhost:6274/...) where you can run tools/list,
inspect each tool's JSON Schema, and call tools interactively with a form built from
that schema. To test read-only mode, set EMP_MCP_READ_ONLY=true in the Inspector's
"Environment Variables" panel before connecting.
Human-in-the-loop (HITL) expectation
This server does not gate write tools on its own confirmation step — it expects the
MCP host to show the user the write tool's resolved input (the employee fields
about to be created/updated/deactivated) before actually calling it, the way most
MCP-aware chat hosts do for consequential tool calls. EMP_MCP_READ_ONLY=true is the
hard backstop for hosts/contexts where that isn't available.
Email-uniqueness interpretation
The requirement is "email unique across active employees." This server enforces it as:
on create, and on update that changes email, reject with DUPLICATE_EMAIL if any
other employee whose status == "active" already holds that email (case-insensitive
compare). Two inactive/on_leave records may share an email with each other or with
what later becomes an active employee's old email — the constraint only looks at
currently-active holders at the moment of the write.
Other documented interpretations
list_employeeslimit > 100: clamped to 100, not rejected.- Deactivating an already-inactive employee: succeeds idempotently; the response
reports
previous_status == new_status == "inactive"rather than erroring. update_employeeandmanager_id: omittingmanager_idmeans "no change." There's currently no way to explicitly clear an employee's manager back tonullviaupdate_employee(the tool can't distinguish "field omitted" from "field explicitly set to null" with this parameter shape) — reassign to a different manager, or editmanager_idat creation time. A future iteration could resolve this with a sentinel/"unset" value if that gap matters for real usage.- SDK-level argument validation: FastMCP validates each tool's arguments (types,
enums) against its generated JSON Schema before our tool code runs. Such failures
are non-crashing and surfaced as
isError: truewith a message startingVALIDATION_ERROR: invalid arguments for <tool>: ..., and are still recorded as onelogs/audit.logline withstatus: "error",code: "VALIDATION_ERROR"— a thin server-level wrapper (_AuditedFastMCPinserver.py) exists specifically to guarantee that, since it doesn't happen for free.
Audit logging
Every tool call appends exactly one JSON line to logs/audit.log (created at
startup if missing) with timestamp, tool, args_summary (PII-light — email
local-part masked, full names omitted), status (ok/error), latency_ms, and
on failure a taxonomy code. Nothing is ever written to stdout — stdout is reserved
for JSON-RPC frames; all logging goes to this file and/or stderr.
Error taxonomy
VALIDATION_ERROR, DUPLICATE_EMAIL, NOT_FOUND, MANAGER_CYCLE, READ_ONLY,
INTERNAL_ERROR — see employee_mcp/errors.py. Every domain failure message has the
shape CODE: human-readable detail, e.g. NOT_FOUND: no employee with id EMP-999.
Tests
python -m pytest tests/test_smoke.py -v
Spins up the real server as a subprocess per test and drives it over stdio with an
mcp.ClientSession, exercising protocol discovery, all 6 tools' success/failure
paths, read-only mode, malformed-argument robustness, and audit-log completeness.
This — not "the LLM said it worked" — is the evidence that the transport contract
holds.
Demo
See demo/demo_transcript.md for a captured run of three flows (search/list, create,
org summary) plus one intentional failure path (duplicate email), against the seed
dataset, with the corresponding logs/audit.log lines shown alongside each call.
Known limitations
- No pagination cursor for
list_employeesbeyondlimit/clamp — fine at the small, in-memory scale this project targets (low thousands of records at most), would need revisiting at larger scale. update_employeecan't clearmanager_idback tonull(see above).- Audit log location defaults to
logs/audit.logand is configurable viaEMP_MCP_AUDIT_PATH, but nothing enforces a 1:1 pairing withEMP_MCP_DATA_PATH— server instances that don't override it share the same default file. - Out of scope by design: no HTTP/SSE transport, no auth/OAuth provider, no
frontend UI, no real HRIS/payroll integration, no stretch tools
(
search_by_skill,get_direct_reports,export_employees_csv, MCP Resources).
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
E2B
Using MCP to run code via e2b.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.