Skip to main content
Glama
README.md
# Setu — Sarvam MCP Server

[![CI](https://github.com/manasasb2000/setu-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/manasasb2000/setu-mcp/actions/workflows/ci.yml)
![Python](https://img.shields.io/badge/python-3.11%2B-blue)
![License: MIT](https://img.shields.io/badge/license-MIT-green)
![Coverage](https://img.shields.io/badge/coverage-87%25-brightgreen)

> 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