Setu — Sarvam MCP Server
by manasasb2000
README.md
# Setu — Sarvam MCP Server
[](https://github.com/manasasb2000/setu-mcp/actions/workflows/ci.yml)



> Open-source **Model Context Protocol** server that exposes Sarvam AI's API surface as
> typed tools, so any MCP-capable agent (Claude Desktop, Cursor, Claude Code, or a custom
> LangGraph agent) can use Sarvam speech / translation / chat / document tools in minutes.
**Live demo & write-up:** https://sarvam-native.netlify.app/setu
---
## Status — Milestone 7 (release-ready)
Feature-complete: all seven Sarvam tools over **stdio + streamable-HTTP**, each
routed through the observability/reliability middleware seam, typed end-to-end,
CI-gated (ruff, mypy --strict, 80%+ coverage, Docker build), containerised, and
**packaged for PyPI** with a tag-triggered release workflow (trusted publishing).
See [`DEMO.md`](DEMO.md) for the 60-second demo storyboard. Everything runs in
mock mode with no key; set `SETU_MODE=live` + `SARVAM_API_KEY` to hit real APIs.
### Architecture
```
MCP client (Claude Desktop / Cursor / Claude Code / LangGraph agent)
│ stdio OR streamable-HTTP (/mcp)
▼
┌──────────────────────── SETU MCP SERVER (FastMCP) ───────────────────────┐
│ Tool layer — typed Pydantic in/out │
│ setu_ping · sarvam_transcribe · sarvam_speak · sarvam_translate │
│ sarvam_transliterate · sarvam_identify_language · sarvam_chat │
│ sarvam_parse_document │
│ │ │
│ ▼ middleware seam (every call) │
│ span(start) → rate-limit(token bucket) → retry/backoff → │
│ dispatch → measure latency → compute INR cost → span attributes │
│ │ │ │
│ ┌──────┴──────┐ ┌──────┴───────┐ │
│ │ MODE: live │ │ OTel spans │ │
│ │ / mock │ │ cost+latency │ │
│ └──────┬──────┘ └──────────────┘ │
│ live │ │ mock │
│ ▼ ▼ │
│ sarvamai SDK canned fixtures (no credits burned) │
└──────────────────────┼───────────────────────────────────────────────────┘
▼
Sarvam APIs — Saaras · Bulbul · Mayura · Sarvam-30B/105B · Parse
```
Every Sarvam call flows through one middleware seam so *every tool call is
observable and priced*:
- **Retries:** exponential backoff with jitter on 5xx/network errors; 4xx never
retried except **429**, which honours the server's `Retry-After`.
- **Rate limiting:** a client-side token bucket per API key; remaining budget is
reported as a span attribute.
- **Cost + latency telemetry:** each call is wrapped in an OpenTelemetry span
carrying `setu.model`, `setu.endpoint`, `setu.latency_ms`, input units, and a
computed `setu.cost_inr` from a maintained INR rate card (`pricing.py`). Spans
export to **stderr** in dev (`SETU_OTEL_EXPORTER=console|none`).
`cost_inr` is also returned on every tool result.
| Milestone | Scope | State |
|---|---|---|
| **1** | Scaffold, config, structlog, FastMCP `setu_ping` over stdio | ✅ |
| **2** | `sarvam_transcribe` + `sarvam_speak`, mock/live dispatch | ✅ |
| **3** | Retry, rate-limit, cost/latency OTel telemetry middleware | ✅ |
| **4** | `translate`, `chat`, `transliterate`, `identify_language`, `parse_document` | ✅ |
| **5** | mypy --strict, 80%+ coverage gate, GitHub Actions CI | ✅ |
| **6** | streamable-HTTP transport, Dockerfile, compose, architecture diagram | ✅ |
| **7** | PyPI packaging + tag-triggered release workflow, demo storyboard | ✅ this repo |
### Tools
| Tool | Model | Key inputs | Output |
|---|---|---|---|
| `setu_ping` | — | `message` | server/version/mode echo |
| `sarvam_transcribe` | Saaras v3 | `audio_base64`\|`audio_url`, `language_code=auto`, `mode=codemix` | `text`, `language`, `confidence`, `cost_inr` |
| `sarvam_speak` | Bulbul v3 | `text`, `target_language_code`, `speaker` | `audio_base64` (WAV), `chars`, `cost_inr` |
| `sarvam_translate` | Mayura v1 | `input`, `target_language_code`, `source_language_code=auto` | `translated_text`, `source_language_code`, `cost_inr` |
| `sarvam_transliterate` | Mayura | `input`, `target_language_code`, `spoken_form` | `transliterated_text`, `cost_inr` |
| `sarvam_identify_language` | text-lid | `input` | `language_code`, `script_code`, `cost_inr` |
| `sarvam_chat` | Sarvam-105B/30B | `messages`, `model`, `tools`, `tool_choice` | `content`, `tool_calls`, `usage`, `cost_inr` |
| `sarvam_parse_document` | Parse | `file_base64`\|`file_url` | `pages`, `fields`, `tables`, `cost_inr` |
Every result also carries `latency_ms` and `request_id`. Try them in mock mode with
no key. To go live, set `SETU_MODE=live` and `SARVAM_API_KEY`, then install the SDK
extra: `pip install -e '.[live]'`. (Live `sarvam_parse_document` uses Sarvam's
job-based Document Digitization API, wired in a later milestone; mock is complete.)
## Quickstart
Once published, install from PyPI and launch with no clone:
```bash
pip install setu-mcp # or: uvx setu-mcp
setu # stdio; or: setu --transport streamable-http
```
To develop from source:
```bash
# 1. Create a virtualenv and install (editable, with dev extras)
python3 -m venv .venv && source .venv/bin/activate
pip install -e '.[dev]'
# 2. Copy the env template (defaults to mock mode — no key needed)
cp .env.example .env
# 3. Run the server over stdio
setu # or: python -m setu.server
# 4. Run the tests
pytest
```
`setu` speaks the MCP protocol on **stdout**, so it looks like it "hangs" — that's correct;
it's waiting for an MCP client. Logs go to **stderr** as JSON. Press Ctrl-C to stop.
## Transports
Setu serves the same tools over two transports from one FastMCP app:
```bash
setu # stdio (local IDE / desktop agents; default)
setu --transport streamable-http # HTTP endpoint at http://127.0.0.1:8000/mcp
```
Configure the HTTP bind with `SETU_HTTP_HOST` / `SETU_HTTP_PORT`.
## Docker
```bash
# Build and run the HTTP server (mock mode) on http://localhost:8000/mcp
docker build -t setu-mcp .
docker run --rm -p 8000:8000 setu-mcp
# Or via compose
docker compose up --build
# Live mode
docker run --rm -p 8000:8000 -e SETU_MODE=live -e SARVAM_API_KEY=sk_... setu-mcp
```
The image is a multi-stage build running as a non-root user; it defaults to the
streamable-HTTP transport. Override with `docker run ... setu --transport stdio`.
## Configuration
All config is environment-driven (`pydantic-settings`); secrets never get hardcoded.
| Variable | Default | Meaning |
|---|---|---|
| `SETU_MODE` | `mock` | `mock` serves fixtures (no credits); `live` calls Sarvam (milestone 2+). |
| `SETU_SERVER_NAME` | `setu` | Name advertised to MCP clients. |
| `SETU_LOG_LEVEL` | `INFO` | structlog level. |
| `SETU_RATE_LIMIT_PER_MINUTE` | `60` | Token-bucket refill rate per API key. |
| `SETU_RATE_LIMIT_TIMEOUT_S` | `10` | Max seconds to wait for a token before erroring. |
| `SETU_MAX_RETRIES` | `3` | Total attempts per Sarvam call (retry cap). |
| `SETU_OTEL_EXPORTER` | `console` | Span exporter: `console` (stderr) or `none`. |
| `SETU_HTTP_HOST` | `127.0.0.1` | Bind host for the streamable-HTTP transport. |
| `SETU_HTTP_PORT` | `8000` | Bind port for the streamable-HTTP transport. |
| `SARVAM_API_KEY` | — | Sarvam key; only needed in `live` mode. Get one at dashboard.sarvam.ai. |
## Connect to Claude Desktop
1. Find the absolute path to the `setu` entrypoint inside your venv:
```bash
source .venv/bin/activate
which setu # e.g. /Users/you/setu-mcp/.venv/bin/setu
```
2. Open Claude Desktop → **Settings → Developer → Edit Config**. This opens
`claude_desktop_config.json`. Add Setu under `mcpServers`:
```json
{
"mcpServers": {
"setu": {
"command": "/ABSOLUTE/PATH/TO/setu-mcp/.venv/bin/setu",
"env": { "SETU_MODE": "mock" }
}
}
}
```
(On Windows the path ends in `\.venv\Scripts\setu.exe`.)
3. **Fully quit and reopen** Claude Desktop. Click the tools/🔌 icon in the chat box —
you should see **setu** listed with the `setu_ping` tool.
4. Ask Claude: *"Use the setu_ping tool with message 'it works'."* You should get back a
typed result showing `ok: true`, the server name, version, and `mode: mock`.
If the server doesn't appear, check Claude Desktop's MCP logs
(`~/Library/Logs/Claude/mcp*.log` on macOS) — Setu's JSON logs on stderr are captured there.
## Layout
```
setu-mcp/
├─ pyproject.toml # package + tooling (ruff, mypy, pytest, coverage)
├─ Dockerfile # multi-stage, non-root, HTTP by default
├─ docker-compose.yml # one-command local run
├─ .github/workflows/ci.yml# ruff + mypy + pytest + docker build
├─ .env.example # config template (mock by default)
├─ src/setu/
│ ├─ config.py # SetuSettings + SarvamSettings (pydantic-settings)
│ ├─ logging.py # structlog → stderr, JSON, request ids
│ ├─ modes.py # live vs mock dispatch
│ ├─ errors.py # typed errors + retryable classification
│ ├─ pricing.py # INR rate card + cost estimator
│ ├─ telemetry.py # OpenTelemetry setup (stderr exporter) + tracer
│ ├─ middleware.py # token-bucket rate limiter + retry/backoff
│ ├─ sarvam_client.py # async wrapper; span→rate-limit→retry→cost seam
│ ├─ fixtures/ # deterministic mock responses
│ ├─ app.py # shared FastMCP app + Sarvam client + settings
│ ├─ tools/ # one module per tool (health, transcribe, speak,
│ │ # translate, transliterate, identify_language,
│ │ # chat, parse_document) — each typed + registered
│ └─ server.py # transport entrypoint (registers tools, runs stdio)
└─ tests/
├─ test_ping.py # health-check + registration
├─ test_tools.py # transcribe/speak (mock) + skipped live smoke test
├─ test_tools_m4.py # translate/transliterate/langID/chat/parse (mock)
└─ test_middleware.py # rate limiter, retry (5xx/4xx/429), pricing, spans
```
## Releasing
Releases are automated. Tagging a version builds the sdist + wheel, creates a GitHub
Release, and publishes to PyPI via **Trusted Publishing** (OIDC — no token stored):
```bash
git tag v0.1.0
git push origin v0.1.0
```
One-time PyPI setup: at [pypi.org → Publishing](https://pypi.org/manage/account/publishing/),
add a trusted publisher for this repo (workflow `release.yml`, environment `pypi`).
## License
MIT © 2026 Manasa SB
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues