Unstop MCP Server
# Unstop MCP Server
`unstop-mcp` is a Python [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server for discovering Unstop hackathons.
It exposes:
- MCP tools for searching hackathons, fetching event details, running location-based discovery, and managing cache state
- MCP resources for read-only snapshots and per-hackathon lookups
- MCP prompts that help an LLM plan searches, compare hackathons, and recommend relevant events
This repository is now MCP-first. The old direct-import Python wrapper API is not the supported public interface anymore.
## What It Supports
- `stdio` transport only
- Unstop hackathons only in v1
- Automatic caching of open hackathons for fast repeated lookups
- Detail enrichment for descriptions, rounds, contacts, registration counts, and views
- Optional location-based search using geocoding
- Deterministic unit tests with mocked upstream behavior
- Optional live smoke tests kept separate from the default suite
## Requirements
- Python 3.12+
- `uv` recommended for local development
- Network access for real Unstop calls
## Installation
### Option 1: `uv` workflow
```bash
git clone https://github.com/your-org/unstop-mcp.git
cd unstop-mcp
uv venv
source .venv/bin/activate
uv pip install -e .
```
### Option 2: plain `pip`
```bash
git clone https://github.com/your-org/unstop-mcp.git
cd unstop-mcp
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install -e .
```
### Verify the install
```bash
unstop-mcp --help
```
You should see the `stdio` transport option.
## Running The Server
Run the server locally over `stdio`:
```bash
unstop-mcp
```
Equivalent module form:
```bash
python -m unstop_mcp
```
The server intentionally supports only:
```bash
unstop-mcp --transport stdio
```
## MCP Surface
### Tools
#### `search_hackathons`
Search Unstop hackathons with filters, sorting, pagination, and optional cache usage.
Arguments:
- `oppstatus`: `open | recent | expired | closed`
- `region`: `online | offline`
- `payment`: `paid | unpaid`
- `teamsize`: `1 | 2 | 3`
- `usertype`: `college_students | fresher | professionals | school_students`
- `sort`: `prize | days_left`
- `direction`: `asc | desc`
- `search`: free-text keyword search
- `page`: `>= 1`
- `per_page`: `1-100`
- `use_cache`: `true | false`
Returns a structured object with:
- `items`: normalized hackathon summaries
- `pagination`: total/current page/last page/per-page/has-more
- `cache`: cache freshness metadata
- `applied_filters`: the validated input used for the call
#### `get_hackathon_details`
Fetch full details for a single hackathon by numeric ID.
Arguments:
- `hackathon_id`
Returns:
- `item`: normalized detail view
- `cache`: metadata describing current server cache state
#### `search_hackathons_by_location`
Find offline hackathons near a location using geocoding and radius filtering.
Arguments:
- `location`
- `radius_km`
- `region`
- `payment`
- `teamsize`
- `usertype`
- `search`
- `sort`: `prize | days_left | distance`
- `direction`
- `page`
- `per_page`
Returns the same normalized structure as `search_hackathons`, plus:
- `location.search_location`
- `location.search_radius_km`
- `location.search_coordinates`
#### `refresh_cache`
Force a rebuild of the cached open-hackathon dataset.
Returns:
- `cache`
#### `get_cache_info`
Inspect cache state without rebuilding the full dataset.
Returns:
- `cache`
### Resources
#### `unstop://cache/info`
Read-only JSON view of cache freshness, TTL, and item counts.
#### `unstop://hackathons/open`
Read-only JSON snapshot of all cached open hackathons.
#### `unstop://hackathons/{hackathon_id}`
Read-only JSON detail view for a specific hackathon. The server serves cached data when possible and falls back to a direct detail fetch when needed.
### Prompts
#### `find_relevant_hackathons`
Guides an LLM to gather missing user preferences, call the right search tool, and summarize the best matches.
Arguments:
- `user_goal`
#### `compare_hackathons`
Guides an LLM to fetch multiple hackathons and compare them.
Arguments:
- `hackathon_ids`
#### `plan_hackathon_search`
Guides an LLM on which tools and resources to call, in what order, for a discovery task.
Arguments:
- `user_request`
## Output Shape
Tool and resource results are normalized into stable JSON-friendly fields rather than returning raw upstream Unstop payloads as the primary contract.
Each hackathon item includes:
- `id`
- `title`
- `status`
- `region`
- `is_paid`
- `public_url`
- `description`
- `prize_amount`
- `prize_summary`
- `filters`
- `required_skills`
- `organisation`
- `address`
- `registration`
- `rounds`
- `contacts`
- `distance_km` when applicable
This keeps the MCP contract predictable even if upstream response shapes vary.
## Client Setup
### Generic MCP client
Point your MCP client at this command:
```bash
unstop-mcp
```
If your client needs an absolute command path, use the one from your environment, for example:
```bash
/absolute/path/to/.venv/bin/unstop-mcp
```
### Claude Desktop
Example `claude_desktop_config.json` entry:
```json
{
"mcpServers": {
"unstop": {
"command": "/absolute/path/to/.venv/bin/unstop-mcp",
"args": []
}
}
}
```
### Codex
Configure a local MCP server entry that launches:
```json
{
"command": "/absolute/path/to/.venv/bin/unstop-mcp",
"args": []
}
```
If your Codex setup manages MCP servers through a separate config file or UI, use the same command and no extra arguments.
## Development
### Project layout
- `src/unstop_mcp/server.py`: FastMCP server definition and MCP registration
- `src/unstop_mcp/service.py`: Unstop fetching, caching, normalization, and geocoding logic
- `src/unstop_mcp/schemas.py`: validated inputs and normalized output models
- `src/unstop_mcp/config.py`: environment-driven runtime configuration
- `tests/`: unit, MCP registration, stdio smoke, and optional live tests
### Environment variables
These are optional:
- `UNSTOP_MCP_TIMEOUT`
- `UNSTOP_MCP_MAX_RETRIES`
- `UNSTOP_MCP_RETRY_DELAY`
- `UNSTOP_MCP_CACHE_TTL_SECONDS`
- `UNSTOP_MCP_DETAIL_WORKERS`
- `UNSTOP_MCP_DETAIL_DELAY`
- `UNSTOP_MCP_GEOCACHE_PATH`
- `UNSTOP_MCP_USER_AGENT`
### How caching works
- Searches for open hackathons can use an in-memory snapshot instead of hitting Unstop on every call
- The snapshot is rebuilt automatically when stale
- `refresh_cache` forces an immediate rebuild
- The geocode cache is persisted to disk so repeated location lookups do not re-query the geocoder unnecessarily
### Geocoding behavior
- Location search depends on `geopy`
- Offline hackathons with explicit coordinates are used directly
- When coordinates are missing, the server tries multiple address variants and caches the result
- Online-only hackathons are excluded from radius searches unless the query explicitly requests `region="online"`
## Testing
Run the default test suite:
```bash
python -m pytest -q
```
What the default suite covers:
- validation and parsing
- normalized search responses
- cache metadata and staleness behavior
- location filtering
- MCP tool/resource/prompt registration
- basic end-to-end `stdio` startup and tool listing
### Optional live smoke tests
These are excluded by default:
```bash
UNSTOP_MCP_RUN_LIVE_TESTS=1 python -m pytest -q -m live
```
Use live tests only when you want to verify the current Unstop integration against the real network.
## Local Validation
Useful local checks:
```bash
python -m pytest -q
python -m unstop_mcp --help
```
If you already use an MCP Inspector, point it at the local `unstop-mcp` command and `stdio` transport to inspect registered tools, resources, and prompts interactively.
## Error Handling
The server validates inputs before making upstream calls.
Common failure cases:
- invalid enum values such as unsupported `oppstatus` or `sort`
- empty or unresolvable location input
- transient or permanent upstream Unstop failures
- missing geocoding support for location search
When possible, tool failures are surfaced as clear MCP-facing validation or request errors.
## Limitations
- `stdio` only in v1
- hackathons only in v1
- upstream Unstop fields and availability can change
- location accuracy depends on source address quality and geocoding quality
- cache refresh can take longer than simple detail fetches because it enriches open hackathons in bulk
## Extending The Server
If you want to add more Unstop opportunity types later, keep this split:
- add new upstream fetch/parse logic in `service.py`
- define new validated contracts in `schemas.py`
- register new MCP surfaces in `server.py`
That keeps transport concerns separate from domain logic.
## License
MIT
TDQS
Scored across 5 tools
Most tools have distinct purposes: get_cache_info inspects cache state, get_hackathon_details fetches a single hackathon, refresh_cache updates the cache, and search_hackathons searches with filters. However, search_hackathons and search_hackathons_by_location could cause some confusion as both search hackathons, though the latter is location-specific.
All tool names follow a consistent verb_noun pattern (e.g., get_cache_info, search_hackathons, refresh_cache). The naming is uniform and predictable, making it easy for an agent to understand the action and target.
With 5 tools, the server is well-scoped for managing hackathon data and cache operations. Each tool serves a clear purpose without being overly sparse or bloated, fitting typical MCP server ranges.
The tools cover core operations for hackathon data (search, get details) and cache management (info, refresh), but there are minor gaps such as no explicit tool for creating or updating hackathon data, which might be outside the server's read-only scope. Overall, it supports common workflows effectively.