pddikti

pddikti

Enables AI agents to query structured Indonesia higher-education data, including universities, study programs, lecturers, students, and aggregate statistics.

Category
Visit Server

README

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

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.

Recommended Servers

playwright-mcp

playwright-mcp

A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.

Official
Featured
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.

Official
Featured
Local
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.

Official
Featured
Local
TypeScript
VeyraX MCP

VeyraX MCP

Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.

Official
Featured
Local
graphlit-mcp-server

graphlit-mcp-server

The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.

Official
Featured
TypeScript
Kagi MCP Server

Kagi MCP Server

An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.

Official
Featured
Python
E2B

E2B

Using MCP to run code via e2b.

Official
Featured
Neon Database

Neon Database

MCP server for interacting with Neon Management API and databases

Official
Featured
Exa Search

Exa Search

A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.

Official
Featured
Qdrant Server

Qdrant Server

This repository is an example of how to create a MCP server for Qdrant, a vector search engine.

Official
Featured