Metadata-Version: 2.4
Name: mcp-jadwal-dokter
Version: 0.2.0
Summary: MCP server exposing dummy Indonesian hospital doctor-scheduling tools
Author: Christian Miracle Rumawung
License: MIT
Project-URL: Homepage, https://mirecodex.my.id
Keywords: mcp,model-context-protocol,healthcare,scheduling,llm,tools,agent
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: mcp[cli]<2,>=1.27
Requires-Dist: pydantic>=2.7
Requires-Dist: tzdata>=2024.1
Provides-Extra: yaml
Requires-Dist: pyyaml>=6.0; extra == "yaml"
Provides-Extra: demo
Requires-Dist: streamlit>=1.40; extra == "demo"
Requires-Dist: anthropic>=0.40; extra == "demo"
Requires-Dist: openai>=1.40; extra == "demo"
Requires-Dist: python-dotenv>=1.0; extra == "demo"
Provides-Extra: backend
Requires-Dist: fastapi>=0.110; extra == "backend"
Requires-Dist: uvicorn[standard]>=0.29; extra == "backend"
Requires-Dist: httpx>=0.27; extra == "backend"
Requires-Dist: anthropic>=0.40; extra == "backend"
Requires-Dist: openai>=1.40; extra == "backend"
Requires-Dist: python-dotenv>=1.0; extra == "backend"
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: pyyaml>=6.0; extra == "dev"
Dynamic: license-file

# mcp-jadwal-dokter

An MCP (Model Context Protocol) server that exposes dummy hospital doctor-scheduling
tools: search doctors, read a schedule, check open slots, and make a booking. It is
built to be driven by an LLM agent, and it ships as a pip package that runs over stdio
with one command.

The server is deterministic: data plus tools, no model and no API key. That is the
point. An MCP server is the tool layer; the model lives in the client. A separate
Streamlit demo (in `demo/`) lets an LLM drive these tools in an agentic chat, for
example to find a pediatrician free on Thursday afternoon and book the slot.

This is a portfolio project (project 11 of a larger set). `PLAN.md` is the full build
plan and design record.

## Architecture

```mermaid
flowchart LR
    User -->|chat| Streamlit
    Streamlit -->|asyncio.run per turn| Agent[Agent loop]
    Agent <-->|prompt plus tools / tool calls| LLM[LLM provider<br/>Anthropic or OpenRouter]
    Agent <-->|stdio MCP| Server[jadwal-dokter MCP server]
    Server --> Tools[search_doctors / get_schedule<br/>check_slot / create_booking]
    Tools --> Seed[(doctors.json, read-only)]
    Tools --> DB[(SQLite bookings)]
```

Everything from the MCP server rightward is the published package and needs no key.
The LLM and the Streamlit client are the demo, and only they read an API key. Each user
turn opens one stdio session to the server, runs the tool-calling loop, and closes it.

## Tools

Eight tools, plus resources and a prompt:

| Tool | What it does |
|------|--------------|
| `get_today()` | The server's date and weekday, so an agent resolves relative phrases ("next Thursday") against the server clock. |
| `search_doctors(specialization?, name?, day?)` | Find doctors by specialization, name substring, and/or practice day. Filters are optional and combine with AND. |
| `get_schedule(doctor_id)` | A doctor's weekly practice blocks and any leave periods. |
| `check_slot(doctor_id, date)` | Available 30-minute slots for a doctor on an ISO date. A day off, a leave date, or a full day return an empty list with an explanatory note, not an error. |
| `create_booking(doctor_id, date, time, patient_name)` | Create a booking. Validates the doctor, practice day, leave, slot, availability, and rejects past dates, with a specific, actionable error when it cannot book. |
| `cancel_booking(booking_id)` | Cancel a booking; the slot frees again, history is kept as `cancelled`. |
| `reschedule_booking(booking_id, new_date, new_time)` | Move a confirmed booking to another slot, keeping its id; the original is untouched on failure. |
| `list_bookings(doctor_id?, date?, patient_name?, status?)` | Filtered list of bookings, including cancelled ones. |

Resources: `jadwal://roster`, `jadwal://specializations`, `jadwal://info`. Prompt:
`book_appointment`. Tool descriptions and error messages are available in Indonesian
(default) or English via `JADWAL_LANG`.

All data is fictional. The bundled example is a made-up hospital ("Rapha Hospital"):
32 doctors across 12 specializations, with varied weekly schedules and deliberate edge
cases (a doctor on leave, a fully booked day, a specialization with a single doctor).
Bookings persist in SQLite, so a booked slot shows as taken on the next check. There is
no connection to any real hospital, patient, or record.

### Bring your own roster

Point `JADWAL_DATA` at a JSON or YAML file (same schema as the bundled example) and the
server schedules against your doctors instead:

```
JADWAL_DATA=/path/to/my-roster.yaml mcp-jadwal-dokter
```

Copy `jadwal_dokter/seed/doctors.example.yaml` as a starting point. Each doctor has an
`id`, `name`, `specialization`, `weekly_blocks` (day in lowercase Indonesian
senin..minggu, `start`/`end` as HH:MM; slots are 30 minutes), and optional
`leave_periods` (inclusive ISO date ranges). A bare list or a `{doctors: [...]}` wrapper
both work. YAML needs the `[yaml]` extra (`pip install "mcp-jadwal-dokter[yaml]"`).

## Requirements

Python 3.10 or newer.

## Install and run the server

During development, from this directory:

```
python -m venv .venv
.venv/Scripts/python -m pip install -e .    # on macOS/Linux: .venv/bin/python
```

Run the server over stdio:

```
mcp-jadwal-dokter
```

The server speaks JSON-RPC on stdio and logs to stderr, so it is meant to be launched
by an MCP client rather than used directly in a terminal.

Once published (see `PLAN.md`, milestone M3), it will run without a manual install:

```
uvx mcp-jadwal-dokter
```

### Use from Claude Desktop

Add the server to `claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "jadwal-dokter": {
      "command": "uvx",
      "args": ["mcp-jadwal-dokter"]
    }
  }
}
```

For a local editable checkout instead of a published package, point the command at the
venv's `mcp-jadwal-dokter` (or `python -m jadwal_dokter.server`) and set `JADWAL_DB` if
you want the bookings file in a specific place.

## Run the demo (agent chat)

The demo is an optional extra with its own dependencies and an API key:

```
.venv/Scripts/python -m pip install -e ".[demo]"
copy .env.example .env          # then set your key, see below
.venv/Scripts/python -m streamlit run demo/streamlit_app.py
```

Set one key in `.env`: `OPENROUTER_API_KEY` (with `JADWAL_PROVIDER=openai`) or
`ANTHROPIC_API_KEY` (with `JADWAL_PROVIDER=anthropic`). Then click "Jalankan skenario
contoh" and watch the agent chain `search_doctors -> check_slot -> create_booking`, with
every call shown in the tool-call log.

A captured end-to-end run of that scenario is in [`SAMPLE_RUN.md`](SAMPLE_RUN.md).

## Full stack (Vue console + chat)

Beyond the Streamlit demo there is a richer web app: a FastAPI backend (`backend/`) that
holds the key and drives the MCP server, and a Vue frontend (`frontend/`) with an agent
Chat and an MCP Console (roster, schedules, bookings, a tool-invoke panel, metrics, and a
tool-call history). See `docs/design-system.md` and `frontend/README.md`.

Run the three processes (the MCP server over HTTP, the backend, and the Vite dev server):

```
# 1. MCP server in HTTP mode
JADWAL_TRANSPORT=http JADWAL_API_KEY=dev-key mcp-jadwal-dokter

# 2. backend (points at the MCP server; holds the provider key from .env)
JADWAL_MCP_URL=http://127.0.0.1:8000/mcp JADWAL_API_KEY=dev-key \
  .venv/Scripts/python -m backend.app

# 3. frontend dev server (proxies /api to the backend)
cd frontend && npm install && npm run dev   # http://localhost:5173
```

In production, `npm run build` emits `frontend/dist`, which the backend serves from the
same origin, so it is one server. When hosting, set `JADWAL_BACKEND_KEY` and
`JADWAL_BACKEND_RATE_LIMIT` to gate `/api`, and `JADWAL_CORS_ORIGINS` for your domain.
Note: the backend has no per-user auth; it is a single-tenant demo (see
`docs/decisions/`).

Note on free models: OpenRouter `:free` models are rate limited per upstream and can
return `429`. The app handles this with retries and a clean error message. For a smooth
run, add a small OpenRouter credit, or use an Anthropic key.

## Tool design decisions

The tools are the product here, so the design choices are deliberate:

1. **Verb-noun names, schema from types.** `search_doctors`, `get_schedule`,
   `check_slot`, `create_booking`. Typed parameters become the JSON input schema and
   docstrings become the descriptions, so the model can pick and fill a tool from the
   schema alone.
2. **Error messages written for the model.** A failed booking says what is wrong and
   what to do next ("slot 14:00 sudah dibooking; slot tersedia: 15:00, 15:30"), not
   "400". The agent recovers instead of giving up.
3. **Compact structured returns.** Each tool returns a small typed object, not a prose
   paragraph. Fewer tokens, and the model reads fields reliably.
4. **Unavailable is an answer, not an error.** A day off, a leave date, or a full day
   come back from `check_slot` as an empty slot list with a note, so the agent can reason
   and try another day. Only genuinely bad input (unknown id, malformed date) raises.
5. **Dummy but persistent bookings.** SQLite so a booking then shows the slot taken, with
   some slots pre-seeded full. State without a real backend.
6. **The LLM is in the client, not the server.** The server is data plus tools, so it
   installs, runs, and tests with no key. That is what an MCP server is, and it is what
   makes the package broadly useful.

## Configuration

| Variable | Purpose | Default |
|----------|---------|---------|
| `JADWAL_DATA` | Path to a custom roster (JSON/YAML). | bundled example |
| `JADWAL_DB` | Path to the SQLite bookings file. | `bookings.db` |
| `JADWAL_LANG` | Tool/error language: `id` or `en`. | `id` |
| `JADWAL_TZ` | IANA timezone for dates and `get_today`. | `Asia/Jakarta` |
| `JADWAL_TODAY` | Pin "today" (ISO date) for reproducible runs. | real clock |
| `JADWAL_TRANSPORT` | `stdio` or `http` (Streamable HTTP at `/mcp`). | `stdio` |
| `JADWAL_HTTP_HOST` / `JADWAL_HTTP_PORT` | Bind address for HTTP mode. | `127.0.0.1` / `8000` |
| `JADWAL_API_KEY` | HTTP mode: require this key (`Bearer`/`X-API-Key`). | none (no auth) |
| `JADWAL_RATE_LIMIT` | HTTP mode: max requests/min per client IP. | `0` (off) |
| `JADWAL_PROVIDER` | Demo only: `openai` or `anthropic`. | `openai` |
| `JADWAL_MODEL` | Demo only: override the model id. | a free OpenRouter model |

The server core needs no API key. The `JADWAL_*` provider variables and the API keys in
`.env.example` are read only by the demo.

## Development

```
.venv/Scripts/python -m pip install -e ".[dev]"
.venv/Scripts/python -m pytest
```

The suite is deterministic and needs no model or key: the server tools are pure, and the
agent-loop tests drive the real MCP server with a stub provider. Each test runs against
an isolated temporary bookings database.

There is also a rerunnable, model-driven eval (`python -m eval.run`) that runs scenarios
through the real agent and checks final state. It needs a working model and reports
free-tier `429`s as skips.

## Status

- Server core and tools: done, tested.
- Streamlit agent demo: done, tested; full scenario verified end to end.
- PyPI publish, CI, and a recorded demo: planned (see `PLAN.md`).
