studyos-mcp-server
by OrbitDev-ux
README.md
# studyos-mcp-server
An **independent MCP (Model Context Protocol) server** that bridges Claude Web
to the **StudyOS Problem Import API**. It forwards batches of newly generated
educational problems to StudyOS and returns a clear, structured result so
Claude can decide whether to keep generating more.
```
Claude Web ──▶ MCP (Streamable HTTP) ──▶ studyos-mcp-server ──▶ StudyOS Import API ──▶ StudyOS Problem Bank
```
This project is **not** StudyOS. It does not contain a database, curriculum
taxonomy, duplicate detection, or validation logic. StudyOS owns all of that.
The MCP server only **authenticates, forwards, retries transiently, and
normalizes the response**.
---
## What it does (and does not do)
| Responsibility | Owner |
| --- | --- |
| Generate problems | Claude Web |
| Basic input shape check + batching guidance | **this MCP server** |
| Server-to-server auth (Bearer) | **this MCP server** |
| Retry on transient failures + normalize result | **this MCP server** |
| Schema / taxonomy / problem validation | StudyOS |
| Duplicate fingerprinting + idempotency | StudyOS |
| Database insertion | StudyOS |
There is **no** database credential, Prisma, Supabase, or direct DB access in
this project — by design.
---
## The `import_problems` tool
Imports a batch of problems into the StudyOS Problem Bank.
### Input
```jsonc
{
"batchId": "claude-web-20260810-0001", // optional; auto-generated if omitted
"source": "claude-web", // optional; defaults to "claude-web"
"targetNewProblems": 1000, // optional; total NEW problems the whole job wants
"problems": [ // required; 1..500 per call
{
"gradeId": "elem-5",
"subjectId": "math",
"unitId": "fraction-mult",
"difficulty": "medium",
"type": "multiple_choice",
"prompt": "3/4 × 2/5의 값은?",
"choices": ["3/10", "2/5", "5/8", "6/20"],
"answerText": "3/10",
"explanation": "분자끼리 곱하고 분모끼리 곱합니다."
}
]
}
```
- **Max 500 problems per call.** Larger jobs must be split into multiple calls.
- Reuse the same `batchId` **only** to retry the exact same batch (StudyOS
handles idempotency). Use a fresh `batchId` for each new batch.
- Unknown extra fields on a problem are passed through to StudyOS untouched.
### Output
```json
{
"ok": true,
"batchId": "claude-web-20260810-0001",
"received": 100,
"accepted": 86,
"duplicates": 12,
"rejected": 2,
"remaining": 914,
"continueRecommended": true,
"message": "Imported batch ...: 86 new, 12 duplicate, 2 rejected (of 100 received)."
}
```
- `remaining` is `null` when StudyOS does not report cumulative progress — in
that case Claude tracks its own running total of `accepted`.
- `continueRecommended` is a hint for whether to generate another batch.
- On `400 / 401 / 403 / 422` the tool returns `isError: true` with a short
message and does **not** retry. Transient failures
(`429 / 500 / 502 / 503 / 504 / network / timeout`) are retried automatically
with backoff, honoring `Retry-After`.
---
## Configuration
All configuration is via environment variables. **Never** put the token in code,
requests, logs, or git.
| Variable | Required | Default | Purpose |
| --- | --- | --- | --- |
| `STUDYOS_IMPORT_TOKEN` | **yes** | — | Server-to-server secret issued by the StudyOS admin. Sent as `Authorization: Bearer <token>`. |
| `STUDYOS_IMPORT_API_URL` | no | production URL | StudyOS Import API endpoint. |
| `TRANSPORT` | no | `http` | `http` (Claude Web / remote) or `stdio` (local MCP Inspector). |
| `PORT` | no | `3000` | HTTP listen port. |
| `ALLOWED_ORIGINS` | no | (empty) | Comma-separated Origin allow-list for `POST /mcp`. Empty = no Origin check. |
| `STUDYOS_REQUEST_TIMEOUT_MS` | no | `30000` | Per-request timeout. |
| `STUDYOS_MAX_RETRIES` | no | `3` | Max retry attempts for transient failures. |
Copy `.env.example` to `.env` for local development (the real token goes in your
host's secret manager, not in the repo).
---
## Run locally
```bash
npm install
npm run build
# HTTP transport (what Claude Web connects to)
STUDYOS_IMPORT_TOKEN=<token> npm start
# -> http://localhost:3000/mcp (health: GET http://localhost:3000/healthz)
# stdio transport (for MCP Inspector)
STUDYOS_IMPORT_TOKEN=<token> npm run start:stdio
```
Inspect with the official MCP Inspector:
```bash
npx @modelcontextprotocol/inspector
```
---
## Deploy
The server speaks **Streamable HTTP (stateless JSON)** and needs a public HTTPS
URL for Claude Web.
### Option A — long-running Node host (Railway / Render / Fly / a container)
Build command `npm run build`, start command `npm start`. Set
`STUDYOS_IMPORT_TOKEN` (and optionally `STUDYOS_IMPORT_API_URL`) as secrets.
Claude Web connects to `https://<host>/mcp`.
### Option B — Vercel (serverless)
This repo includes `api/mcp.ts` and `vercel.json`. Deploy to Vercel, set the
env vars in the project settings, and Claude Web connects to:
```
https://<your-deployment>.vercel.app/api/mcp
```
---
## Connect from Claude Web
1. Deploy the server and confirm `GET /healthz` returns `{ "ok": true }`.
2. In Claude (web) → **Settings → Connectors → Add custom connector**.
3. Enter the MCP URL:
- Node host: `https://<host>/mcp`
- Vercel: `https://<deployment>.vercel.app/api/mcp`
4. Save. Claude can now call `import_problems`.
Then a user can simply ask, e.g.:
> 초5 수학 분수 단원 문제 1000개 만들어서 StudyOS 문제은행에 입고해줘.
Claude generates problems, calls `import_problems` in batches of ≤500, reads
`accepted` / `remaining`, and repeats until the target of new problems is met.
---
## Security
- The token is read lazily from the environment and is **never** logged,
returned, or placed in error messages. A defensive redactor strips it from any
string just in case.
- No database credentials are used or accepted (no `DATABASE_URL`, `DIRECT_URL`,
`SUPABASE_*`, Prisma, or Postgres client).
- The server exposes exactly one tool (`import_problems`) and one upstream call
(the StudyOS Import API) — no arbitrary request execution.
---
## Testing
```bash
npm run typecheck # tsc --noEmit
npm test # vitest (schema, retry, normalization, redaction, tool e2e)
npm run build # tsc
```
The suite covers: valid/empty/oversized/invalid batches, 401/403/422 no-retry,
429/500 retry with `Retry-After`, network + timeout handling, response
normalization (partial success, duplicates, remaining, field-name variants),
token redaction, and a full in-memory MCP client → tool round trip.
---
## Project layout
```
studyos-mcp-server/
├── api/mcp.ts # Vercel serverless entry (Option B)
├── vercel.json
├── src/
│ ├── index.ts # entry: HTTP (default) + stdio transports
│ ├── server.ts # createServer(): registers tools
│ ├── tools/importProblems.ts
│ ├── studyosClient.ts # HTTP client: auth, retry, timeout, normalization, redaction
│ ├── schemas.ts # Zod input schemas (basic shape check only)
│ ├── constants.ts # config + retry policy
│ └── types.ts
└── test/ # vitest suites
```
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues