Skip to main content
Glama
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
```