Skip to main content
Glama
andikadevs

pddikti

by andikadevs

PDDikti API

Bun + Hono API providing structured access to Indonesia higher-education data (PDDikti): universities, study programs, lecturers, students, and aggregate statistics.

This service is a typed proxy in front of a private upstream PDDikti data source, with auto-generated OpenAPI docs.

Requirements

Related MCP server: YOKATLAS MCP Server

Setup

bun install
cp .env.example .env   # set UPSTREAM_BASE_URL; supply Cloudflare creds at runtime (see below)

Run

bun run dev     # hot reload
bun run start   # start server

Open http://localhost:3000.

Docker setup

Run as three containers — the API + a FlareSolverr sidecar (hands-off Cloudflare clearance) + Redis (response cache) — via the provided docker-compose.yml:

docker compose up -d          # build + start api (:3000) + flaresolverr
docker compose logs -f api    # follow API logs
docker compose down           # stop

What it wires:

  • api — built from the multi-stage Dockerfile (oven/bun:1.3-alpine, frozen-lockfile deps-only install). Reads .env; compose also sets SOLVER_ENABLED=true, SOLVER_URL=http://flaresolverr:8191/v1, and CONFIG_STORE_PATH=/data/runtime-config.json.

  • flaresolverrghcr.io/flaresolverr/flaresolverr, internal only (not published to the host); its headless browser solves the Cloudflare challenge on a 403.

  • redisredis:7-alpine (password + AOF persistence), internal only; the API's cache.

  • config-data / redis-data volumes — persist the solved cf_clearance and the cache across restarts.

Only the API is reached (via the reverse proxy); FlareSolverr + Redis stay internal. For a public-facing setup put a TLS-terminating reverse proxy in front, and set PUBLIC_BASE_URL, API_KEY, and CONFIG_PASSWORD accordingly.

Single image, no solver (refresh clearance manually via /#/config):

docker build -t pddikti .
docker run -p 3000:3000 --env-file .env pddikti

Configuration

All config is read once from the environment via src/config.ts (the only place that touches process.env). Bun auto-loads .env. Key variables:

Variable

Default

Purpose

PORT

3000

Listen port

PUBLIC_BASE_URL

http://localhost:3000

Absolute base for docs links +secure cookies

UPSTREAM_BASE_URL

PDDikti API base (https://pddikti.kemdiktisaintek.go.id/api)

UPSTREAM_TIMEOUT_MS

15000

Per-request upstream timeout

API_KEY

`` (off)

Gate/api/* (X-API-Key or SPA session cookie); empty = public

SESSION_TTL_S

86400

Session/config cookie lifetime

CONFIG_PASSWORD

`` (off)

Enables the/#/config page; empty = disabled (404)

CONFIG_STORE_PATH

./runtime-config.json

Where runtime overrides + solved cookie persist

SOLVER_ENABLED

false

Auto-solve Cloudflare on403 via FlareSolverr

SOLVER_URL

FlareSolverr/v1 endpoint

CACHE_ENABLED / CACHE_TTL_S

false / 600

Optional Redis response cache

CREDIT_LINE

Data © PDDikti

Attribution injected into every JSON response

See .env.example for the full list.

Web UI

  • GET / — flat React PT profile explorer: search a university, pick one, and see its profile, stats, cost, study duration, study programs, all lecturers, student search, facilities, and name history. The selected university is carried in the URL hash (#/pt/<id>). Bundled natively by a single Bun.build (JS + Tailwind v4 CSS via bun-plugin-tailwind), no Vite. Client code is organized into components/ (UI + views), hooks/ (use-endpoint, use-selected-pt, use-debounced-value), and lib/ (constants, format, theme). Requires a configured upstream (below) to show live data.

Docs

  • Overview: GET /api/

  • Swagger UI: /api/docs

  • ReDoc: /api/redoc

  • OpenAPI spec: /api/openapi.json (live)

  • MCP tools: /#/mcp (readable view) · /mcp/tools (JSON catalog — the MCP analogue of the OpenAPI spec)

  • Static spec: openapi.json in the repo, for offline agent reference. Regenerate after route changes with bun run openapi:export (set PUBLIC_BASE_URL to stamp the servers URL, e.g. PUBLIC_BASE_URL=https://<host> bun run openapi:export).

MCP (AI agent access)

The same data is exposed to AI agents over the Model Context Protocol. Tools are consolidated and enum-driven (~10): search_pddikti, get_university, get_university_study_programs, get_university_lecturers, get_study_program, get_study_program_lecturers, get_lecturer, get_student, get_statistics, list_study_programs_by_field. Each routes through the same proxy egress, cache, and credit envelope as the REST API.

Two transports:

  • stdiobun run mcp (runs src/mcp/stdio.ts). For local clients.

  • Streamable HTTPPOST /mcp on the running server. For remote/hosted clients. Always public (no API key) — even when /api/* is gated by API_KEY — so browser clients like Claude.ai, which can't send a static X-API-Key, can connect.

Claude Code (CLI)

Local (stdio) — from the repo root, env comes from .env:

claude mcp add pddikti -- bun run /abs/path/to/pddikti/src/mcp/stdio.ts

Or commit a project-scoped .mcp.json (already included — fill the env):

{
  "mcpServers": {
    "pddikti": {
      "command": "bun",
      "args": ["run", "./src/mcp/stdio.ts"],
      "env": {
        "UPSTREAM_BASE_URL": "https://pddikti.kemdiktisaintek.go.id/api",
        "CONFIG_STORE_PATH": "/abs/path/to/pddikti/runtime-config.json"
      }
    }
  }
}

Pointing CONFIG_STORE_PATH at the same runtime-config.json the config page writes lets the stdio server reuse that Cloudflare clearance (or set SOLVER_ENABLED/SOLVER_URL to auto-resolve). See Upstream config below.

Remote (HTTP) against a running instance:

claude mcp add --transport http pddikti https://<host>/mcp

Claude Desktop

Edit claude_desktop_config.json (Settings → Developer → Edit Config) — same shape as .mcp.json above under mcpServers. Restart the app; tools appear under the 🔌 menu.

Claude.ai (web)

Settings → ConnectorsAdd custom connector → URL https://<host>/mcp. The web UI cannot send a static X-API-Key/Basic-auth header, so /mcp is intentionally public (no key) regardless of API_KEY — the web connector works out of the box. Only the /api/* REST surface is key-gated.

Quick test

bun run mcp:inspect    # MCP Inspector against the stdio server

# Or hit the HTTP endpoint directly (initialize handshake):
curl -s https://<host>/mcp -X POST \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"1"}}}'

Upstream config

This API proxies the official PDDikti backend, which lives at .../api on the main domain and is guarded by Cloudflare bot management. Only the base URL goes in .env:

UPSTREAM_BASE_URL=https://pddikti.kemdiktisaintek.go.id/api

Cloudflare needs a cf_clearance cookie + the exact User-Agent it was issued to (a mismatched/absent UA → 403). You provide these at runtime, not in .env, two ways:

  • Manually via the config page — set CONFIG_PASSWORD, open the Konfigurasi view (/#/config, a React page backed by the /config/* JSON API), paste the cf_clearance + User-Agent (or a full DevTools Copy as cURL, auto-parsed). Saved to CONFIG_STORE_PATH, applied live, persists across restarts. Refresh when it expires.

  • Automatically via the solver — see Auto-solver below; a 403 self-heals with no manual step.

(An optional UPSTREAM_HEADERS JSON env map is still accepted as a fallback baseline, but is overridden by the runtime config and normally left unset.)

The upstream wraps payloads as {status,data} and returns opaque base64url ids (with = padding, sent raw in the path) — both handled in lib/upstream.ts / lib/encoding.ts.

Without UPSTREAM_BASE_URL, /api/* returns a clean 503 envelope (no data).

Auto-solver (optional, hands-off clearance)

cf_clearance expires, so instead of refreshing it by hand you can let the app do it — server-deployable via FlareSolverr (a headless-browser challenge solver that runs in its own container). Set SOLVER_ENABLED=true + SOLVER_URL (the FlareSolverr /v1 endpoint). On a 403, the proxy calls FlareSolverr to clear the challenge, saves the fresh cf_clearance + User-Agent (to CONFIG_STORE_PATH), and retries the request once. Left off, refresh manually via the /#/config page.

Run both with the provided docker-compose.yml:

docker compose up -d   # starts the API + a flaresolverr sidecar

The browser lives inside FlareSolverr, so this works on a headless server (no display).

Architecture

  • Bun + Hono (OpenAPIHono), exported as export default { fetch, port } — no Bun.serve, Vite, or Express.

  • Single egress: every upstream call goes through lib/upstream.ts (auth headers, timeout, {status,data} unwrap, error mapping, 403 auto-refresh), with an optional Redis cache (lib/cache.ts). Routes never fetch directly.

  • Zod-first: one schema drives both request validation and OpenAPI generation; strict TypeScript, no any (upstream payloads are opaque z.unknown()).

  • Two frontends, one core: the REST /api/* routes and the in-process MCP server (/mcp) both call lib/ directly — identical egress, cache, and credit envelope, no second hop.

  • Native React SPA: bundled by a single Bun.build (+ Tailwind v4 CSS-first), served by Hono; components/ + hooks/ + lib/ layout, one shared sticky Header.

  • Definition of done: bun run typecheck (clean) + bun test (green), no network in tests.

Full conventions live in CLAUDE.md.

Scripts

Script

Purpose

bun run dev

Hot-reloading dev server

bun run start

Run server

bun run mcp

Run the MCP server over stdio

bun run mcp:dev

MCP server (stdio) with hot reload

bun run mcp:inspect

Launch MCP Inspector against the stdio server

bun test

Run tests

bun run typecheck

tsc --noEmit

Project conventions

See CLAUDE.md.

License

MIT — open source, free to use, copy, modify, and distribute.

Disclaimer

For educational and learning purposes only. This is a personal, non-commercial study project.

  • Not affiliated with or endorsed by PDDikti, Kemdiktisaintek, or Cloudflare. All upstream data is © PDDikti; names belong to their respective owners.

  • Provided "as is", with no warranty of accuracy, availability, or fitness (see the MIT terms).

  • No liability for misuse. The authors accept no responsibility for how this software is used or for any damages arising from it — you use it entirely at your own risk.

  • Your responsibility. You must comply with all applicable laws and the terms of service of any system it touches (including the upstream's terms and its anti-bot/rate-limit protections). Do not use it to overload, disrupt, mass-scrape, or abuse any service, or to handle personal data unlawfully.

See LICENSE for the full text.

F
license - not found
-
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/andikadevs/pddikti'

If you have feedback or need assistance with the MCP directory API, please join our Discord server