Skip to main content
Glama
README.md
# yokatlas-mcp-server

A Python [MCP](https://modelcontextprotocol.io) server exposing [YÖK ATLAS](https://yokatlas.yok.gov.tr)
"tercih kılavuzu" (preference guide) reference-data tools: provinces, universities, and program
groups (each tagged with its exam score type). Built on [FastMCP](https://github.com/modelcontextprotocol/python-sdk)
with `pydantic-settings` config, a standard `{ok, data, error}` tool envelope, and a rate-limited
HTTP client with retries.

## ⚠️ Sorumluluk Reddi Beyanı (Disclaimer)

Bu proje tamamen **deneysel** amaçlarla oluşturulmuştur. Yapay zeka modelleri hata yapabilir;
bu sunucunun ürettiği hiçbir sonuç %100 doğru veya güncel olarak kabul edilmemelidir. Hangi
okulu veya bölümü seçeceğinize karar verirken mutlaka bir rehber öğretmen, eğitim danışmanı
veya alanında uzman bir kişiden **profesyonel destek alın**.

Bu proje, yalnızca puanınıza göre hangi okul/bölümlerin mevcut olabileceği konusunda size bir
**fikir vermek** amacıyla oluşturulmuştur; resmî bir tercih/danışmanlık aracı değildir. Burada
elde ettiğiniz sonuçları her zaman [resmî YÖK ATLAS](https://yokatlas.yok.gov.tr) verileriyle
karşılaştırıp doğrulayın.

Bu proje ve geliştiricisi, bu sunucunun ürettiği bilgilere dayanılarak yapılan üniversite/bölüm
tercihlerinden veya bu tercihlerin sonuçlarından **hiçbir şekilde sorumlu tutulamaz**. Tercih
sürecinde yapılan hatalardan dolayı proje sahibine sorumluluk yüklenemez.

A Turkish version of this same warning is returned in every tool response's `disclaimer` field
(see below) — the server instructions tell the calling model to always surface it to the user
verbatim, not just leave it in this file.

**Bu projeyi (bu MCP sunucusunu) indirerek, kurarak, çalıştırarak veya Claude Desktop, Codex,
ChatGPT ya da başka bir istemci üzerinden bu sunucuya bağlanıp kullanarak, yukarıdaki
sorumluluk reddi beyanının tamamını okuduğunuzu, anladığınızı ve kabul ettiğinizi beyan etmiş
sayılırsınız.**

## MCP İstemcilerine Bağlanma (Claude Desktop, Codex, ChatGPT)

### Herkese Açık Sunucu (kurulum gerektirmez)

Bu sunucu zaten `https://yokatlas.aykutbuyukkaya.com/mcp` adresinde yayında. Kendi bilgisayarınızda
hiçbir şey kurmadan/çalıştırmadan, aşağıdaki "Custom Connector" adımlarında bu URL'yi kullanarak
doğrudan bağlanabilirsiniz. Kendi kopyanızı çalıştırmak isterseniz aşağıdaki yerel kurulum
adımlarını izleyebilirsiniz.

Sunucu varsayılan olarak `stdio` üzerinden çalışır; Claude Desktop, Claude Code ve Codex gibi
istemciler sunucuyu doğrudan yerel bir alt süreç olarak başlattığı için ayrı bir HTTP kurulumu
gerekmez. ChatGPT ise yalnızca internetten erişilebilen (HTTPS) bir MCP sunucusuna
bağlanabildiği için ya yukarıdaki herkese açık adresi ya da kendi deploy ettiğiniz bir adresi
kullanmanız gerekir.

Kendi kopyanızı çalıştırmak için önce bağımlılıkları kurun:

```bash
git clone <bu-repo-url>
cd yokatlas-mcp-server
uv sync
```

### Claude Desktop

Claude Desktop'ın ayar dosyasını açın:
- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
- Windows: `%APPDATA%\Claude\claude_desktop_config.json`

`mcpServers` altına aşağıdaki girdiyi ekleyin (yolu kendi klonladığınız dizinle değiştirin) ve
Claude Desktop'ı yeniden başlatın:

```json
{
  "mcpServers": {
    "yokatlas-mcp-server": {
      "command": "uv",
      "args": ["--directory", "/path/to/yokatlas-mcp-server", "run", "yokatlas-mcp-server"]
    }
  }
}
```

Claude Code CLI kullanıyorsanız aynı işi tek satırla yapabilirsiniz:

```bash
claude mcp add yokatlas-mcp-server -- uv --directory /path/to/yokatlas-mcp-server run yokatlas-mcp-server
```

**Custom Connector (uzak/HTTP sunucu) ile bağlanma:** Kendi bilgisayarınızda hiçbir şey
kurmadan, herkese açık sunucuyu kullanarak bağlanabilirsiniz:

1. Claude Desktop'ta **Settings → Connectors**'a gidin.
2. **Add custom connector**'a tıklayıp `https://yokatlas.aykutbuyukkaya.com/mcp` adresini MCP
   sunucu URL'si olarak girin ve ekleyin.
3. Yeni bir sohbette bu connector'ı etkinleştirip araçları kullanmaya başlayın.

Kendi deploy ettiğiniz bir sunucuyu kullanmak isterseniz (bkz. [Docker](#docker)), 2. adımda
kendi `https://<alan-adiniz>` adresinizi girmeniz yeterlidir.

### Codex CLI (OpenAI)

`~/.codex/config.toml` dosyasına aşağıdaki bloğu ekleyin:

```toml
[mcp_servers.yokatlas-mcp-server]
command = "uv"
args = ["--directory", "/path/to/yokatlas-mcp-server", "run", "yokatlas-mcp-server"]
```

Codex CLI sürümünüz destekliyorsa aynı satırı komut satırından da ekleyebilirsiniz:

```bash
codex mcp add yokatlas-mcp-server -- uv --directory /path/to/yokatlas-mcp-server run yokatlas-mcp-server
```

### ChatGPT (Connectors / Developer mode)

ChatGPT yerel bir komut çalıştıramadığı için sunucunun internetten erişilebilir bir HTTPS
adresi olması gerekir. Herkese açık sunucuyu kullanabilir ya da kendi kopyanızı
`MCP_TRANSPORT=streamable-http` ile (bkz. [Docker](#docker)) bir alan adının arkasına deploy
edebilirsiniz, ardından:

1. ChatGPT'de **Settings → Connectors → Advanced → Developer mode**'u açın (bu özellik hesap
   planınıza göre farklı bir menüde görünebilir).
2. **Add custom connector** ile `https://yokatlas.aykutbuyukkaya.com/mcp` adresini (veya kendi
   deploy ettiğiniz `https://<alan-adiniz>` adresini) MCP sunucu URL'si olarak girin.
3. Sohbette bu connector'ı etkinleştirip `get_university_cities`, `get_universities`,
   `get_university_programs` ve `search_university_programs` araçlarını kullanmaya başlayın.

> `command`/`args` yolları `uv`'nin sistem PATH'inizde bulunduğunu varsayar; değilse `command`
> alanına `uv`'nin tam yolunu (örn. `/opt/homebrew/bin/uv`) yazmanız gerekir.

Diğer HTTP tabanlı istemciler için de `MCP_TRANSPORT=streamable-http` (veya `sse`) ayarlayıp
`http://<MCP_HOST>:<MCP_PORT>` adresine bağlanmanız yeterlidir.

## Tools

| Tool | Upstream endpoint | Filters |
| --- | --- | --- |
| `get_university_cities` | `GET /api/tercih-kilavuz/universite-iller` | `name_contains`, `limit` |
| `get_universities` | `GET /api/tercih-kilavuz/universiteler` | `name_contains`, `limit` |
| `get_university_programs` | `GET /api/tercih-kilavuz/universite-programlar` | `name_contains`, `score_type`, `limit` |
| `search_university_programs` | `POST /api/tercih-kilavuz/search` | see below |

`get_university_cities`, `get_universities`, and `get_university_programs` are reference-data
lookups. `search_university_programs` is the actual preference search — it returns program
placements (university + program + quota + fee + scholarship + prior guide's minimum success
rank / score cutoff), and its `university_ids`, `program_group_ids`, and `city_codes` filters are
meant to be resolved first from the three lookup tools' `id`/`ilKodu` fields, not guessed.

`search_university_programs` also filters by `program_type` (`46`/`"LİSANS"`, `47`/`"ÖNLİSANS"`),
`university_type` (`DEVLET`, `VAKIF`, `"VAKIF MYO"`, `KKTC`, `"YURTDIŞI KAMU"`, `"YURTDIŞI VAKIF"`),
`scholarship_type` (`0`/`"ÜCRETSİZ"`, `154`/`"ÜCRETLİ"`, `158`/`"BURSLU"`, `155`/`"%25 İNDİRİMLİ"`,
`156`/`"%50 İNDİRİMLİ"`), `study_mode` (`86`/`"ÖRGÜN ÖĞRETİM"`, `182`/`"UZAKTAN ÖĞRETİM"`,
`203`/`"AÇIK ÖĞRETİM"`), an exact `guide_code` (to fetch one specific placement), a
`min_success_rank`/`max_success_rank` range, and standard `page`/`size` (max 50)/`sort_by`/
`direction` pagination. All of the above accept either the raw id/code or the Turkish label —
with or without diacritics, in any case (`"lisans"`, `"LİSANS"`, and `46` are equivalent). Set
`include_conditions_text=True` to also get each result's full admission-condition paragraphs
(`conditions_text`); left off by default since they can be long.

`score_type` accepts `SAY`, `EA`, `SÖZ` (or ASCII `SOZ`), `DİL` (or ASCII `DIL`), and `TYT`.
Name filters are case-insensitive substring matches that handle Turkish dotted/dotless I
correctly (e.g. `name_contains="istanbul"` matches `İSTANBUL`).

Every tool returns a standard envelope so callers can branch on success/failure without parsing
exceptions. Every response also carries a `disclaimer` field with the Turkish warning above
(experimental/AI-generated data, verify against the official YÖK ATLAS, get professional
guidance):

```json
{"ok": true, "data": {...}, "error": null, "disclaimer": "Bu sonuç deneysel..."}
{"ok": false, "data": null, "error": {"code": "INVALID_INPUT", "message": "...", "retryable": false}, "disclaimer": "Bu sonuç deneysel..."}
```

This full envelope (including `disclaimer`) is always available as `structuredContent`. But
relying only on a JSON field inside `structuredContent`, plus the server's MCP `instructions`
telling the model to relay it, turned out not to be reliable in practice — not every client
forwards `instructions` to the model, and a field buried in a large JSON blob is easy for a
model to summarize away. So each tool's actual text output (`content`) leads with the
disclaimer as its own explicit, unmissable directive block, ahead of the JSON data — see
`envelope.to_call_tool_result()`.

## Project layout

```
src/yokatlas_mcp_server/
  __init__.py
  settings.py                    # pydantic-settings config, loaded from env vars / .env
  errors.py                       # domain exceptions + {code, message, retryable} error payloads
  envelope.py                      # shared {ok, data, error, disclaimer} response helpers
  text_utils.py                     # Turkish-aware casefold/upper + int/limit validation
  enums.py                           # search filter value normalization (labels <-> ids)
  rate_limiter.py                     # async token-bucket limiter for the upstream client
  models.py                            # City, University, ProgramGroup dataclasses
  yokatlas_client.py                    # TercihKilavuzuClient: retries, rate limiting, error mapping
  server.py                              # FastMCP app: tool/resource/prompt registration, entry point
  tools/
    __init__.py
    get_university_cities.py
    get_universities.py
    get_university_programs.py
    search_university_programs.py

tests/
  test_scaffold.py                # settings + transport validation
  test_envelope.py                 # {ok, data, error, disclaimer} response helpers
  test_server_tool_dispatch.py     # real FastMCP tool-call dispatch, not execute_* directly
  test_enums.py                    # filter value normalization
  test_yokatlas_client.py          # HTTP client behavior against a mocked transport
  test_get_university_cities_tool.py
  test_get_universities_tool.py
  test_get_university_programs_tool.py
  test_search_university_programs_tool.py
```

## Getting started

```bash
uv sync
cp .env.example .env
uv run pytest
uv run yokatlas-mcp-server
```

The default transport is `stdio`, so `uv run yokatlas-mcp-server` will sit waiting for an MCP
client to speak to it over stdin/stdout — that's expected. Use `mcp dev` for an interactive
inspector instead:

```bash
uv run mcp dev src/yokatlas_mcp_server/server.py
```

## Adding a new tool

1. Add a method to `TercihKilavuzuClient` in `yokatlas_client.py`, or create a new
   `<domain>_client.py` module following the same pattern (owns retries, rate limiting via
   `RateLimiter`, and translates HTTP/network failures into the `errors.py` exception hierarchy).
2. Create `tools/<name>.py` with a `Protocol` describing the client methods the tool needs and an
   `async def execute_<name>(...)` that validates input, calls the client, and returns the
   envelope via `envelope.make_success_response` / `make_error_response` (copy
   `tools/get_university_cities.py` as a starting point).
3. Register it in `server.py` with `@mcp.tool(name=..., description=...)`, describing each
   parameter with a pydantic `Field(description=...)` so MCP clients get useful tool docs.
4. Add tests mirroring `test_get_university_cities_tool.py` (fake client, no network) and, if you
   added a client method, `test_yokatlas_client.py` (real client, `httpx.MockTransport`, no
   network).

## Docker

```bash
uv lock
docker build -t yokatlas-mcp-server .
docker run --rm -p 8000:8000 yokatlas-mcp-server
```

The image defaults `MCP_TRANSPORT` to `streamable-http` and binds to `0.0.0.0:8000`. `uv lock`
must be run once (and whenever dependencies change) to produce `uv.lock`, which the Dockerfile
installs from with `--frozen` for reproducible builds.

## Configuration

See [.env.example](.env.example) for all supported environment variables: MCP transport/host/port,
logging, and the YÖK ATLAS API's base URL, timeouts, retry policy, and rate limit.

## License

MIT — see [LICENSE](LICENSE).

TDQS

A4.3/5.0

Scored across 4 tools

Disambiguation5/5

Each tool targets a distinct resource: cities, universities, program groups, and placement search. The search tool explicitly relies on IDs resolved from the other three, making boundaries clear and preventing misselection.

Naming Consistency5/5

All tool names follow a consistent verb_noun pattern in snake_case: get_* for reference lookups and search_* for the complex query. The pattern is predictable and uniform.

Tool Count5/5

Four tools cover the core domain without bloat or thinness. Each tool has a distinct role, and the count fits the purpose of a reference/search server well within the ideal 3-15 range.

Completeness5/5

The surface covers the full workflow: resolving reference data (cities, universities, programs) and performing the main placement search. No obvious gaps or dead ends exist for the stated purpose.

Maintenance

ActivityStale
ResponsivenessNo issues