Crescender MCP Server
# Crescender MCP Server
Model Context Protocol server for Crescender's public read API. Connects AI clients (Claude Desktop, Cursor, custom agents) to a school's assets, loans, members, and asset-comms threads — all read-only, scope-token-authenticated, and bound to a single school per token.
> **Status: 0.0.x — pre-publish.** This package has not yet been published to npm or pushed to GHCR. The publish pipelines exist but are gated to manual triggers; the first release ships when the source has been pressure-tested locally. For now, run from a clone or a `pnpm link` (see [Local development](#local-development) below).
## What it gives an AI
Six read-only tools, each backed by an `/api/v1/*` endpoint:
| MCP tool | Returns |
|---|---|
| `list_schools` | The single school the token is bound to (single-item array) |
| `get_asset` | Full asset detail by id — model, serial, location, role, current loan |
| `search_assets` | Up to 50 assets matching `q` / `item_class` / `category` / `status` |
| `get_loans_for_asset` | Loan history for one asset (current + past) |
| `list_members` | Member directory — id, role, status, external_id (no PII) |
| `list_asset_threads` | Asset-comms threads with status filter (read-only — vendor-token issuance is NOT exposed via MCP) |
Reads only. No `update_*` / `create_*` tools in v1.
## Configuration
Two environment variables, one of them required:
| Var | Default | Notes |
|---|---|---|
| `CRESCENDER_API_TOKEN` | — | **Required.** Issue from `https://app.crescender.com.au/school/<id>/settings/integrations` (the "Integrations" tile under your school's Settings). Format `crsc_<random>`. |
| `CRESCENDER_API_URL` | `https://app.crescender.com.au` | Override only if you're pointing at a non-production deployment. |
| `CRESCENDER_MCP_LOG_LEVEL` | `info` | One of `debug` / `info` / `warn` / `error`. Logs go to stderr (stdout is reserved for the MCP protocol). |
| `CRESCENDER_MCP_HEALTH` | `0` | Set to `1` in container deployments to enable the `GET /health` HTTP endpoint on `PORT` (default `3030`). |
| `CRESCENDER_API_TIMEOUT_MS` | `15000` | Per-request upstream timeout. |
## Connecting Claude Desktop (once published)
Once the first version is on npm:
```jsonc
// ~/Library/Application Support/Claude/claude_desktop_config.json (macOS)
// %APPDATA%\Claude\claude_desktop_config.json (Windows)
{
"mcpServers": {
"crescender": {
"command": "npx",
"args": ["-y", "@crescender/mcp-server"],
"env": {
"CRESCENDER_API_TOKEN": "crsc_..."
}
}
}
}
```
Then restart Claude Desktop. The six tools should appear in the MCP picker.
## Connecting Cursor (once published)
Cursor reads MCP config from `.cursor/mcp.json` in your workspace, or globally from settings. The shape is the same as Claude Desktop's `mcpServers` block.
## Local development
```bash
# Install
git clone https://github.com/lincalinca/crescender-mcp-server.git
cd crescender-mcp-server
pnpm install
# Build
pnpm build
# Smoke test (talks to stdin/stdout — Claude Desktop / Cursor will spawn it
# the same way)
CRESCENDER_API_TOKEN=crsc_... node dist/index.js
```
To wire your local clone into Claude Desktop without publishing:
```jsonc
{
"mcpServers": {
"crescender-local": {
"command": "node",
"args": ["/absolute/path/to/crescender-mcp-server/dist/index.js"],
"env": {
"CRESCENDER_API_TOKEN": "crsc_..."
}
}
}
}
```
## Container
```bash
# Build (the published image is built by .github/workflows/publish-image.yml)
docker build -t crescender-mcp-server:dev .
# Run with the in-container /health endpoint enabled
docker run --rm -i \
-e CRESCENDER_API_TOKEN=crsc_... \
-e CRESCENDER_MCP_HEALTH=1 \
-p 3030:3030 \
crescender-mcp-server:dev
```
Once published, the image will be available at `ghcr.io/lincalinca/crescender-mcp-server`.
## Statelessness contract
- No in-memory token cache: validation hits the upstream API on every call (revocation is immediate).
- No filesystem state: stdout/stderr only.
- Config is env-driven, parsed once at boot, frozen.
- `/health` (when enabled) returns `{ ok, api_reachable, version }` and probes the upstream `/api/v1/health` to verify reachability — not just "this process is alive."
- Graceful shutdown: SIGTERM / SIGINT trigger a 30-second drain, then force-exit.
This is what makes the container k8s-ready without any k8s-specific code. When the orchestration decision is eventually made (ASK / AKS / GKE / Fly / Railway / etc.), nothing changes here.
## License
MIT — see [LICENSE](./LICENSE).
## Related
- [`@crescender/crescender-core`](https://github.com/lincalinca/crescender-core) — the platform itself, which serves the `/api/v1` endpoints this MCP wraps.
- OpenAPI spec: `https://app.crescender.com.au/api/v1/openapi.json`
TDQS
Scored across 6 tools
Each tool targets a distinct resource or action: get_asset and search_assets are clearly different (detail vs. search), and loan history, threads, members, and schools are all separate concerns. No overlapping purposes.
All tools consistently follow the 'verb_noun' pattern in lowercase snake_case (e.g., get_asset, list_asset_threads, search_assets). The naming is uniform and predictable.
6 tools is well-scoped for an asset management server. Each tool covers a core area without being overwhelming or insufficient.
The tool surface is heavily read-only: assets have get and search but no create/update/delete; loans only show history; threads only list; members and schools only list. Missing basic write operations for the domain.