Content
# Tool List
[](https://github.com/ghdtjdwn/ssuMCP/actions/workflows/ci.yml)
> 🇺🇸 English version: [README.en.md](README.en.md)
> 🧩 **Campus AI Platform** (One of 4 services) · **ssuMCP** · [ssuAI](https://github.com/ghdtjdwn/ssuAI) · [ssuAgent](https://github.com/ghdtjdwn/ssuAgent) · [ssu-ai-service](https://github.com/ghdtjdwn/ssu-ai-service) · 🟢 [Live](https://ssuai.vercel.app)
An open-source server providing Soongsil University campus information using the **MCP (Model Context Protocol) standard tool**. It also serves as a REST API server for the [ssuAI](https://github.com/ghdtjdwn/ssuAI) web client.
> **MCP Endpoint**: `https://ssumcp.duckdns.org/mcp`
> **Grafana**: `https://ssumcp.duckdns.org/grafana`
## Why We Created This
We were tired of manually searching for daily information like school meals, library seats, grades, and schedules on the university portal. Instead of just creating a crawler, we decided to build an MCP server that can be called by LLMs (Large Language Models) as a tool, allowing for reuse in chatbots, IDEs, and automation pipelines.
## Connecting
### Claude Desktop
`%APPDATA%\Claude\claude_desktop_config.json` (Windows) /
`~/Library/Application Support/Claude/claude_desktop_config.json` (macOS):
```json
{
"mcpServers": {
"ssuMCP": {
"url": "https://ssumcp.duckdns.org/mcp"
}
}
}
```
Restart and check the tool list in the chat window.
### Cursor / Other MCP Clients
`.cursor/mcp.json`:
```json
{
"mcpServers": {
"ssuMCP": {
"url": "https://ssumcp.duckdns.org/mcp"
}
}
}
```
### Self-Hosting
To run on your own infrastructure, use the published launcher for a one-line execution (Java 21+ required — download JAR from GitHub releases):
```bash
npx ssumcp
```
### n8n Workflow Automation
In `n8n/`, there is an example workflow using the ssuMCP public API as a source — new announcements → Discord notifications (5-minute polling), weekly announcements and meal reports (monthly 09:00 cron). The core logic like reservation state machines and auth is owned by the server, while n8n is responsible for "connecting completed services for operational automation".
## Tool List (52 tools)
### Public Tools — No Authentication Required
**Meals, Facilities, Library**
| Tool | Description |
|------|------|
| `get_today_meal` | Today's meal menu |
| `get_meal_by_date` | Meal by date |
| `get_meal_weekly` | Weekly meal |
| `get_dorm_weekly_meal` | Residence hall weekly meal |
| `search_campus_facilities` | Campus facility search |
| `get_library_seat_catalog` | Library seat catalog |
| `search_library_book` | Central library book search |
**Academic Policy RAG**
| Tool | Description |
|------|------|
| `get_academic_calendar` | Academic calendar query |
| `find_academic_calendar_events` | Academic calendar event search by month/keyword |
| `classify_academic_question` | Academic question intent classification |
| `search_academic_policy_sources` | Official academic policy source search |
| `get_academic_policy_brief` | Official academic policy summary |
| `check_scholarship_policy` | Scholarship policy check |
| `list_academic_policy_sources` | Academic RAG official source list |
**Announcements**
| Tool | Description |
|------|------|
| `get_recent_notices` | Latest school announcements |
| `search_notices` | Announcement keyword search |
| `list_notice_categories` | Announcement category list |
| `get_notice_detail` | Announcement detail |
| `get_active_notices` | Ongoing announcements |
| `get_department_notices` | Department announcements |
### Personal Tools — Authentication Required
Authentication flow:
1. `start_auth(provider="SAINT")` → `loginUrl` + `mcpSessionId` received
2. Open `loginUrl` in browser to log in with Soongsil University account
3. Pass `mcp_session_id` parameter when calling personal tools
**Session Management**
| Tool | Description |
|------|------|
| `start_auth` | Login URL issuance (provider: SAINT / LMS / LIBRARY) |
| `get_auth_status` | Session status check |
| `logout_provider` | Logout of specific provider |
| `logout_all` | Full session logout |
**u-SAINT (provider: SAINT)**
| Tool | Description |
|------|------|
| `get_my_schedule` | Schedule (semester specified) |
| `get_my_grades` | Grades and cumulative GPA |
| `get_my_chapel_info` | Chapel attendance status |
| `check_graduation_requirements` | Graduation requirements fulfillment |
| `evaluate_graduation_with_policy` | Graduation requirements + official policy |
| `get_my_scholarships` | Scholarship benefits |
| `simulate_gpa` | This semester's expected grades → cumulative GPA prediction |
**LMS (provider: LMS)**
| Tool | Description |
|------|------|
| `get_my_assignments` | Current semester unsubmitted assignments/quizzes |
| `get_my_lms_terms` | User's LMS registered semester list |
| `get_lms_dashboard` | Unsubmitted assignments, schedule, and announcements dashboard |
| `get_my_lms_courses` | User's LMS course list |
| `get_my_lms_materials` | Non-video lecture materials (PDF, PPT, etc.) |
| `prepare_lms_material_export` | Material export preparation |
| `confirm_lms_material_export` | Export confirmation and 20-minute valid download link |
| `export_all_lms_materials` | Automatic collection and preview return |
**Library (provider: LIBRARY)**
| Tool | Description |
|------|------|
| `get_library_seat_status` | Floor-wise seat status (2nd, 5th, 6th floors) |
| `get_library_available_seats` | Overall reading room live per-seat available seats summary |
| `get_room_available_seats` | Specific reading room per-seat status |
| `recommend_library_seats` | Preference-based seat recommendation |
| `prepare_reserve_library_seat` | Seat reservation preparation |
| `wait_for_library_seat` | Seat waiting registration |
| `get_library_wait_status` | Latest seat waiting status |
| `cancel_library_wait` | Seat waiting cancellation |
| `get_my_library_seat` | Current reserved seat |
| `prepare_swap_library_seat` | Seat swap preparation |
| `prepare_cancel_library_seat` | Seat return preparation |
| `confirm_action` | Write operation final execution |
| `get_my_library_loans` | Library loan status |
## Architecture
The system consists of three services:
```
User Browser
│ HTTPS
▼
ssuAI (Next.js · Vercel) Claude Desktop / Cursor / IDE
│ SSE stream (chatbot) │ MCP protocol
│ REST /api/* (dashboard) │
▼ ▼
ssuAgent (Python · LangGraph · k3s) ssuMCP (Spring Boot 4 · k3s)
│ Natural language understanding, tool selection │ MCP server /mcp
│ HITL interrupt management │
└──────── MCP tools/call ──────────► Service Layer
Connectors (fault-tolerant)
│
┌───────────┼───────────┐
▼ ▼ ▼
Pyxis LMS u-SAINT
(library) (learningx) (rusaint FFI)
```
`ssuMCP` is an MCP tool server — responsible for atomic domain tools, direct connection to school systems, fault tolerance, and action auditing. `ssuAgent` is a LangGraph orchestrator that handles natural language understanding, tool composition, and HITL interrupts. The two services are connected only through the MCP protocol, allowing for independent deployment.
## Engineering Notes
Implementation challenges and rationale are recorded here. Detailed information can be found in [Troubleshooting Highlights](docs/troubleshooting-highlights.md).
### u-SAINT: SAP WebDynpro Reverse Engineering → rusaint FFI Transition
u-SAINT is based on SAP NetWeaver WebDynpro, which involves complex stateful interactions. After an 8-day attempt at direct HTTP reverse engineering, we switched to using the verified Rust implementation [rusaint](https://github.com/EATSTEAK/rusaint) with JNA (Java Native Access) for JVM FFI integration.
### WAF Cookie + CookieManager Isolation
The university's security equipment (WAF) was found to downgrade sessions to anonymous (ANON) when a specific cookie was missing, as detected through browser DevTools packet capture. The initial approach of only passing `MYSAPSSO2` was modified to also preserve the `WAF` cookie.
In the LMS Canvas 5-hop SSO chain, a manual cookie merging approach was found to contaminate cookies between subdomains. To implement browser-level domain-specific cookie isolation, a separate `java.net.CookieManager` was equipped for each request.
### Single-flight Cache
SAP WebDynpro has a stateful structure that requires navigating through pages one by one using the previous button. A 1-hour TTL cache was implemented, and a single-flight pattern was used to handle duplicate requests that occur simultaneously during a cold start. Only the first request is sent to the upstream, and the rest share the result.
### Pyxis per-seat API Reverse Engineering
The Pyxis library API, which had no official documentation, was reverse-engineered using the browser DevTools Network tab to track down the `GET /pyxis-api/1/api/rooms/{roomId}/seats` endpoint. A real-time per-seat inquiry tool was implemented by mapping four states (available/occupied/away/inactive) using combinations of `isActive`, `isOccupied`, and `seatChargeState` in the response.
### Asynchronous Reservation Concurrency — Intent Queue · Per-seat Distributed Lock · SSE (EPIC 4·5)
If seat reservations were made with synchronous calls, duplicate writes would occur on the upstream (Pyxis) when multiple requests for the same seat converge, and LLM calls would be blocked for an extended period. The `wait_for_library_seat` was queued as an asynchronous intent, and a background worker processed it.
Concurrency was serialized using a per-seat **Redisson distributed lock** (`LibraryDistributedLockClient`), with Postgres `SELECT … FOR UPDATE` serving as the source of truth for consistency, and the lock used only for efficiency. The waiting/result state was pushed in real-time using **Redis RTopic fan-out based SSE** (`GET /api/library/reservations/wait/events/{intentId}`).
### Spring AI Reflection Workaround
`McpServer.SyncSpecification.tools` was `package-private final`, so there was no public API for injecting tool annotations (`readOnlyHint`, `destructiveHint`). The auto-configure bean was replaced with `@Primary McpSyncServerCustomizer`, and the tool list was reconstructed using reflection. This is a temporary bridge until Spring AI provides a public API.
### Observability 3-pillars (Metrics · Distributed Tracing · Central Logging)
**Metrics**: Spring Boot Actuator + Micrometer Prometheus registry. The operating cluster was deployed with ArgoCD `monitoring` Application for kube-prometheus-stack, and the `ssuai-backend` ServiceMonitor scraped `/actuator/prometheus` — RED 13-panel dashboard + 3 PrometheusRules (5xx>5%, p99>3s, circuit breaker OPEN). Grafana is available at `https://ssumcp.duckdns.org/grafana`.
**Distributed Tracing + Central Logging** (ADR 0069): Micrometer Observation bridge → OTel → OTLP → **Tempo**, Logback JSON → Promtail → **Loki**. Each provider attempt included a `llm.provider.call` span, making the **10-provider fallback sequence visible in the tracing timeline**. Java agent (zero-code) was used instead of code-level bridge to express domain-specific spans. **Prod live** — Tempo/Loki/Promtail were deployed with ArgoCD (single-binary, single ARM node budget) and backend emit (`json-logs` + 0.1 sampling) was enabled to query TraceQL `service.name=ssuai` traces and Loki logs in Grafana.
## Security Principles
- **Secret Value Non-Logging**: Passwords, session cookies, JWTs, and API keys are not recorded in logs.
- **Authentication Boundary Separation**: Even for aggregated data like seat status, authentication is included in the cache key to prevent anonymous callers from receiving authentication results.
- **Read-only MCP**: Most tools have `readOnlyHint=true`, and `logout_*` has `destructiveHint=true`.
- **Write Tool Design Principle**: State-changing tools like reservations and cancellations are implemented only with a 2-step confirmation pattern of `prepare_*` + `confirm_action`. (ADR 0015)
### Security Hardening (2026-06 Remediation)
Several independent security reviews were conducted, and the findings were addressed by **distinguishing between actual, false positives, and already-fixed issues through code comparison**. Key controls included SSO callback code exchange (ADR 0095), library session permanent cookie key (ADR 0096), MCP session resolution OAuth-sub ownership guard (session fixation/privilege escalation prevention, ADR 0056), CSRF Origin/Referer verification filter (SameSite=None maintenance, ADR 0057), prod setting fail-fast (ADR 0058), reservation audit single source of truth + fail-closed seat lock (ADR 0059), `/api/chat` chatbot read-only (write is only allowed for ssuAgent HITL, ADR 0060), per-IP rate limit + input limit (ADR 0061), and supply chain SHA pin + k8s pod-security (ADR 0062).
## Technology Stack
| Category | Technology |
| --- | --- |
| Language / Framework | Java 21 + Kotlin 2.3, Spring Boot 4.0 |
| MCP Implementation | Spring AI 1.1 (Streamable HTTP, SDK 0.18.3 — 4 artifacts batch pin) |
| Concurrency / Distributed | Redisson 4.5 (per-seat distributed lock · RTopic multi-pod fan-out · leader election), PostgreSQL `SELECT FOR UPDATE SKIP LOCKED` |
| Resilience | Resilience4j 2.3 core (provider-specific circuit breaker · retry · rate limiter · bulkhead) |
| u-SAINT Interoperability | rusaint — JNA 5.18 for Rust/UniFFI library connection to JVM |
| Academic Policy Search | **Hybrid RAG** — lexical + embedding cosine RRF (k=60) fusion, `academic_embeddings` persistent cache (optional pgvector HNSW profile for ANN capability proof, ADR 0070) |
| Authentication | JJWT 0.13 (HS256, **gson serializer for Jackson decoupling**) · OAuth2.1 resource server · AES-256-GCM |
| Observability | Micrometer/Prometheus metrics + OTel/Tempo distributed tracing + Logback JSON/Loki central logging (ADR 0069) |
| Testing | JUnit 5 · **Testcontainers** (real PG16/Redis7 integration test) · **JaCoCo coverage ratchet** · WireMock 3 · k6 (ADR 0068) |
| Infrastructure | Oracle Cloud ARM64 · k3s · Traefik · ArgoCD (Image Updater GitOps) · Helm · GHCR (ARM64) · Prometheus/Grafana |
| Database | PostgreSQL 17 + Flyway (vendor-split migration) |
| Crawling | Jsoup 1.22 |
## Local Development
### Execution (mock mode — no external network required)
```bash
./gradlew bootRun
# http://localhost:8080
```
MCP Inspector for tool verification:
```bash
npx @modelcontextprotocol/inspector
# Transport: Streamable HTTP URL: http://localhost:8080/mcp
```
### Execution (with real data)
```bash
cp .env.example .env
SSUAI_CONNECTOR_MEAL=real ./gradlew bootRun
```
### Verification
```bash
./gradlew test # unit + (Docker if available) Testcontainers real PG/Redis integration test
./gradlew test jacocoTestReport jacocoTestCoverageVerification # coverage ratchet gate (LINE 0.70 floor)
./gradlew build
```
> ~1,030 tests (0 failures). Integration tests with containers are automatically skipped (`disabledWithoutDocker`) if Docker is not available, so offline builds are green, and CI enforces the gate.
## Environment Variables
Key variables are referenced in `.env.example`. Actual values are not committed to the repository.
| Variable | Description |
| --- | --- |
| `SSUAI_JWT_SECRET` | HS256 signature key (32 bytes or more). If not set, it is updated on restart. |
| `SSUAI_CREDENTIAL_ENCRYPTION_KEY` | AES-GCM session encryption key |
| `SSUAI_CONNECTOR_MEAL` | `mock` (default) or `real` |
| `SSUAI_OPENROUTER_API_KEY`, ... | LLM provider keys (each optional) |
## Backend / AI Portfolio Highlights
Key decisions that address actual engineering challenges:
### 1. Pyxis External API Fault Tolerance (EPIC 2)
Resilience4j was used to implement 4-layer protection for the university library API (`oasis.ssu.ac.kr`), which becomes unstable during exam periods.
- **Key Decision**: Read/write asymmetry. Only reads are retried (3 times with exponential backoff) — writes are not idempotent and risk duplicate reservations.
- **Measurement**: Shared CircuitBreaker "pyxis" — slidingWindow 20, failureThreshold 50%, 30s wait, 3 probe half-open.
- **Grafana**: `resilience4j_circuitbreaker_state{name="pyxis"}` · failure rate · slow call rate panels.
### 2. Write Tool Safety Design — Action Audit State Machine (EPIC 3)
`prepare_* → confirm_action` 2-step confirmation pattern. The user must explicitly confirm; otherwise, no actual Pyxis call occurs.
- **Duplicate Confirm Prevention**: `action_audit` row PREPARED state is atomically converted using `SELECT FOR UPDATE`. A second confirm will not find the row and returns an error.
- **Write Timeout Recovery**: After a timeout, `getCurrentCharge` (GET, idempotent) checks the actual Pyxis state → updates `action_audit`.
- **k6 Experiment Results**: 100 concurrent bursts for the same seat → SUCCESS 2 · FAILURE_RACE 98 · ghost reservation 0.
### 3. k6 Load Testing (EPIC 1)
Measured values were used to design improvements for EPIC 2·3·4.
| Scenario | Results |
| --- | --- |
| `get_library_seat_status` 50 RPS · 5 minutes | p95 **19.7ms** · error 0% · cache hit rate ~99% |
| Write burst 100 concurrent (same seat) | SUCCESS 2 · RACE 98 · ghost 0 |
| Write burst 100 concurrent (different seats) | SUCCESS **100** · throughput 100% |
> Full experiment design · results · interpretation: [`docs/performance/library-agent-load-test.md`](docs/performance/library-agent-load-test.md)
### 4. rusaint JNA FFI — u-SAINT SAP WebDynpro Interoperability
u-SAINT is based on SAP NetWeaver WebDynpro, so standard HTTP reverse engineering failed within 8 days. A validated Rust implementation, rusaint, was connected to the JVM using JNA.
- SmartID SSO callback `sToken`·`sIdno` received → passed to `RusaintUniFfiClient` → one-time exchange code issued → `POST /api/auth/exchange` issues web session cookies.
- Passwords do not reach the server (processed by SmartID).
### 5. LangGraph Multi-agent System — ssuAgent (EPIC 6)
Separated into a distinct Python service from ssuMCP, connected only through the MCP protocol.
- **Supervisor → Library·Academic·LMS Agents** 3-way routing.
- **HITL Interrupt**: If `prepare_reserve_library_seat` result includes `actionId`, graph interrupt → user confirmation → `confirm_action` resumes.
- **LLM Multi-fallback**: Groq llama-3.3-70b → Gemini 2.5 Flash → OpenRouter llama-3.3-70b (Groq has a free daily quota of 14,400, making it the top priority).
### 6. MCP Session 3-tier Authentication (ADR 0036, 0037)
Independent sessions for each university system (SAINT·LMS·Library) are managed with a single `mcp_session_id`.
```
OAuth sub → McpAuthSession (7d Postgres persistence)
│
├─ provider-specific links (SAINT / LMS / LIBRARY)
└─ transport non-binding → session ID reused on reconnection
```
sToken·LMS cookies·Pyxis-Auth-Token are stored encrypted with AES-256-GCM.
## Documents
- [Architecture](docs/architecture.md)
- [Failure Scenarios](docs/failure-scenarios.md)
- [Interview Q&A](docs/interview-qa.md)
- [MCP Tools and Authentication Flow](docs/mcp-tools.md)
- [Security Policy](docs/security.md)
- [Performance Report (EPIC 1 k6)](docs/performance/library-agent-load-test.md)
- [Troubleshooting Highlights](docs/troubleshooting-highlights.md)
- [Deployment Runbook](deploy/README.md)
## Related Projects
**[ssuAI](https://github.com/ghdtjdwn/ssuAI)** — Next.js web client (chatbot UI + dashboard)
**[ssuAgent](https://github.com/ghdtjdwn/ssuAgent)** — Python LangGraph multi-agent orchestrator
## License
MIT — [LICENSE](LICENSE)
MCP Config
Below is the configuration for this MCP Server. You can copy it directly to Cursor or other MCP clients.
mcp.json
Connection Info
You Might Also Like
No recommendations available