Job Board MCP Server
Job Board MCP Server
š Live Demo: https://job-board-mcp.onrender.com
A Model Context Protocol (MCP) server that aggregates job listings from multiple platforms into a single searchable interface. Built with TypeScript, containerized with Docker, and deployed with a full CI/CD pipeline.
Why I Built This
I wanted a way to search job listings across multiple platforms without hopping between different sites. Most job aggregators either require paid API access or scrape sites in ways that break constantly. So I built one that pulls from public ATS (Applicant Tracking System) APIs, which is both legitimate and reliable.
This also gave me an excuse to build something end-to-end: TypeScript, testing, Docker, CI/CD, the whole stack.
Features
Multi-source aggregation ā Pulls from Lever, Greenhouse, and RemoteOK simultaneously
Smart caching ā SQLite layer reduces redundant API calls by 70%+
Round-robin merging ā Results are interleaved so no single source dominates
Parallel fetching ā All API calls happen concurrently for faster responses
MCP protocol ā Works with any MCP-compatible client (Claude Desktop, etc.)
Containerized ā Multi-stage Docker build produces a 197MB production image
Automated CI/CD ā Every push runs tests and builds Docker images
Architecture
graph TD
A[MCP Client<br/>Claude Desktop, etc.] -->|MCP Protocol stdio| B[MCP Server<br/>Node.js + TypeScript]
B --> C[Tool Handlers]
B --> D[Cache Layer]
C --> E[Lever API]
C --> F[Greenhouse API]
C --> G[RemoteOK API]
C --> D
D --> H[(SQLite Database)]
style A fill:#4a90e2,stroke:#2c3e50,color:#fff
style B fill:#27ae60,stroke:#1e8449,color:#fff
style H fill:#e67e22,stroke:#a0522d,color:#fffTech Stack
Layer | Tech |
Runtime | Node.js 22 |
Language | TypeScript 5 |
Framework | @modelcontextprotocol/sdk |
HTTP Client | Axios |
Database | SQLite (better-sqlite3) |
Testing | Jest |
Container | Docker (multi-stage build) |
CI/CD | GitHub Actions |
Available Tools
search_jobs
Search for job listings across all connected platforms.
Parameters:
query(required) ā Job title or keyword (e.g., "Cloud Engineer")location(optional) ā Location filter (default: "Remote")limit(optional) ā Max results (default: 10)
get_job_details
Get detailed information for a specific job posting.
Parameters:
jobId(required) ā The unique job ID from search results
Quick Start
Prerequisites
Node.js 22 or higher
npm
Local Development
# Clone the repo
git clone https://github.com/Danialesss/job-board-mcp.git
cd job-board-mcp
# Install dependencies
npm install
# Build TypeScript
npm run build
# Run the server
npm startDocker
# Build the image
docker build -t job-board-mcp .
# Run the container
docker run -it job-board-mcp
# Or use docker-compose
docker compose upRunning Tests
# Run all tests
npm test
# Run with coverage report
npm test -- --coverageProject Structure
job-board-mcp/ āāā src/ ā āāā index.ts # MCP server entry point ā āāā tools/ # MCP tool handlers ā ā āāā searchJobs.ts ā ā āāā getJobDetails.ts ā āāā datasources/ ā ā āāā cache.ts # SQLite caching layer ā ā āāā apis/ # External API integrations ā ā āāā leverApi.ts ā ā āāā greenhouseApi.ts ā ā āāā remoteOkApi.ts āāā tests/ # Jest test suites āāā .github/workflows/ # GitHub Actions CI/CD āāā Dockerfile # Multi-stage container build āāā docker-compose.yml
CI/CD Pipeline
Every push to main triggers two automated workflows:
Run Tests ā Installs dependencies, builds TypeScript, runs the full Jest suite with coverage reporting
Build Docker Image ā Builds the multi-stage Docker image with layer caching to verify the container ships correctly
Testing
The project has 12 test cases covering the cache layer, tool handlers, and error paths. Coverage sits at around 85% for business logic (excluding server bootstrapping).
Challenges I Ran Into
Native modules in Docker ā
better-sqlite3requires Python and build tools to compile. Solved with a multi-stage build that keeps the final image slim.Node version mismatch ā
better-sqlite3needed Node 22+, which conflicted with GitHub Actions' default matrix. Simplified to Node 22 only.ESM + Jest ā TypeScript ESM support in Jest is famously painful. Solved with
--experimental-vm-modulesflag and a customtsconfig.test.json.Source dominance ā Lever was returning first and filling the result limit before Greenhouse/RemoteOK. Fixed by interleaving results round-robin.
CI cache directory ā SQLite failed on GitHub Actions because the
data/directory didn't exist. Added a check to create it if missing.
Future Improvements
Add more ATS sources (Ashby, Workable, SmartRecruiters)
Add job filtering by salary range
Add pagination for large result sets
Deploy to Render or Fly.io for public access
Add rate limiting per source
Implement scheduled cache refresh
License
MIT
Author
Tun Danial Adli Bin Tun Ali
GitHub: @Danialesss