Job Board MCP Server
# Job Board MCP Server
š **Live Demo:** [https://job-board-mcp.onrender.com](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
```mermaid
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:#fff
```
## Tech 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
```bash
# 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 start
```
### Docker
```bash
# Build the image
docker build -t job-board-mcp .
# Run the container
docker run -it job-board-mcp
# Or use docker-compose
docker compose up
```
### Running Tests
```bash
# Run all tests
npm test
# Run with coverage report
npm test -- --coverage
```
## Project 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:
1. **Run Tests** ā Installs dependencies, builds TypeScript, runs the full Jest suite with coverage reporting
2. **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-sqlite3` requires Python and build tools to compile. Solved with a multi-stage build that keeps the final image slim.
- **Node version mismatch** ā `better-sqlite3` needed 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-modules` flag and a custom `tsconfig.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](https://github.com/Danialesss)
TDQS
Scored across 2 tools
The two tools have clearly distinct purposes: one searches for job listings and the other retrieves details for a specific job by ID. There is no overlap or ambiguity between them.
Both tools follow the same verb_noun snake_case pattern: search_jobs and get_job_details. The naming is predictable and consistent.
Two tools feels thin for a job board server. While they cover the basic search-and-detail flow, a typical job board server would likely benefit from additional tools such as listing recent jobs, filtering by platform, or paginating results.
The server covers the core read-only workflow of searching for jobs and viewing details, but it lacks other common operations like browsing all recent jobs without a query, pagination controls, or platform-specific listing. The surface is functional but minimal.