Skip to main content
Glama
MSPbotsAI

oitvoip-mcp

by MSPbotsAI
README.md
# oitvoip-mcp

MCP server for **Oitvoip** (hosted VoIP/UCaaS reseller platform, built on
**NetSapiens** — API host pattern is `{tenant-pbx-host}/ns-api/`). Exposes
the NetSapiens ns-api's domain, reseller, device, subscriber, and CDR
methods as MCP tools.

> Naming note: MSPbots' own integration is registered as "Oitvoip"
> (`subjectCode=NS` — short for NetSapiens); the underlying API and all
> official documentation reference "NetSapiens" / "ns-api". This MCP
> covers exactly the 5 methods MSPbots itself has configured.

## Overview

- Stateless HTTP service. No credentials are ever persisted — each request
  supplies its own credentials via headers, used only for the lifetime of
  that single request.
- Supports concurrent requests; per-request credential isolation is done via
  Python `contextvars`, not a global/shared client instance.
- Entry points: `POST /mcp` (MCP protocol) and `GET /health` (health check).
- Default port: `8080` (configurable via `MCP_HTTP_PORT`).

## Authentication

NetSapiens uses a standard OAuth2 **password grant**:

```
POST https://{site}/ns-api/oauth2/token/
  grant_type=password&client_id=...&client_secret=...&username=...&password=...
-> {"access_token": "...", "expires_in": 3600, "token_type": "Bearer", ...}
```

The resulting `access_token` is valid for 1 hour, but this server
re-authenticates fresh on every single tool call rather than caching it
across MCP requests — nothing is cached or persisted. Every real ns-api
call then sends `Authorization: Bearer <access_token>`.

### HEADER 授权参数说明

| Header | 类型 | 是否必填 | 默认值 | 枚举值 | 字段描述 | Example |
|---|---|---|---|---|---|---|
| `X-Oitvoip-Site` | string | 是 | 无 | 无 | 租户 PBX 主机名(不含协议前缀) | `pbx.example.com` |
| `X-Oitvoip-Client-Id` | string | 是 | 无 | 无 | NetSapiens OAuth2 API Client ID | `58900.mspbot` |
| `X-Oitvoip-Client-Secret` | string | 是 | 无 | 无 | NetSapiens OAuth2 API Client Secret | `a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6` |
| `X-Oitvoip-Username` | string | 是 | 无 | 无 | Subscriber 登录名(含域名后缀) | `1000@example` |
| `X-Oitvoip-Password` | string | 是 | 无 | 无 | 对应密码 | `••••••••` |

Missing any header returns `401`:
```json
{
  "error": "Missing credentials",
  "message": "This server requires the X-Oitvoip-Site, X-Oitvoip-Client-Id, X-Oitvoip-Client-Secret, X-Oitvoip-Username, X-Oitvoip-Password headers",
  "required_headers": ["X-Oitvoip-Site", "X-Oitvoip-Client-Id", "X-Oitvoip-Client-Secret", "X-Oitvoip-Username", "X-Oitvoip-Password"],
  "optional_headers": []
}
```

An invalid credential or an authenticated-but-under-scoped subscriber
account surfaces as a tool-level `unauthorized` error envelope (message
includes the vendor's own detail, e.g. `Invalid Scope [APP001]`), not an
HTTP-level error from this server — see Known Gaps.

## Environment Variables

| Variable | 类型 | 是否必填 | 默认值 | 说明 |
|---|---|---|---|---|
| `MCP_HTTP_PORT` | int | 否 | `8080` | HTTP 监听端口 |
| `MCP_HTTP_HOST` | string | 否 | `0.0.0.0` | HTTP 监听地址 |

## MCP Endpoint

- `POST /mcp` — MCP protocol (streamable HTTP transport)
- `GET /health` — health check, returns `{"status": "ok"}` (pure local probe, does not call the vendor API)

## Tool List

| Tool | 功能 | 参数 |
|---|---|---|
| `oitvoip_get_domains` | 列出该 reseller 账号下所有已开通的域名(租户) | 无 |
| `oitvoip_get_resellers` | 获取指定域名的 reseller 级别详情 | `domain`(必填) |
| `oitvoip_get_devices` | 列出指定域名下已注册的 SIP 设备/终端 | `domain`(必填) |
| `oitvoip_get_subscribers` | 列出指定域名下的用户/分机 | `domain`(必填) |
| `oitvoip_get_cdr2` | 获取指定域名、指定日期范围内的通话详单(CDR) | `domain`、`start_date`、`end_date`(均必填) |

Responses are the vendor's JSON (array or object depending on the method),
serialized compactly (no indentation, `ensure_ascii=False`). If a response
would exceed ~20,000 characters, the largest list field is truncated and
the result includes `truncated: true` plus the original count, rather than
returning an unbounded blob. All 5 tools are read-only (`readOnlyHint`) —
there are no write/delete tools in this service.

On error, tools return a structured JSON error envelope instead of raising:

```json
{"error": {"code": "unauthorized", "message": "...", "retryable": false}}
```

`code` is one of `not_configured` / `unauthorized` / `not_found` /
`invalid_argument` / `rate_limited` / `upstream_error`; `retryable`
indicates whether the Agent may safely retry (true for `rate_limited` and
`upstream_error`).

## 测试示例

```bash
# Health check
curl -s http://localhost:8080/health

# Call a tool via the MCP protocol (streamable HTTP) — requires an
# initialize handshake first per the MCP spec; abbreviated example below
# shows the tool-call request body only:
curl -s -X POST http://localhost:8080/mcp \
  -H "X-Oitvoip-Site: pbx.example.com" \
  -H "X-Oitvoip-Client-Id: 58900.mspbot" \
  -H "X-Oitvoip-Client-Secret: <your-client-secret>" \
  -H "X-Oitvoip-Username: 1000@example" \
  -H "X-Oitvoip-Password: <your-password>" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "mcp-session-id: <session-id-from-initialize>" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
      "name": "oitvoip_get_subscribers",
      "arguments": {"domain": "example.58900.service"}
    }
  }'
```

**Live-verified** (2026-07-30) against a real tenant, all 5 tools called
end-to-end through this running server: `oitvoip_get_subscribers` returned
real subscriber/extension records; `oitvoip_get_devices` returned real
registered SIP devices (Polycom endpoints, live registration state);
`oitvoip_get_cdr2` returned real call detail records for the given date
range. `oitvoip_get_domains` and `oitvoip_get_resellers` correctly reached
the API and surfaced a clean, expected `401 Invalid Scope [APP001]`
tool-level error — the provided test credentials are a subscriber-level
account (`scope: "Office Manager"`), which does not carry domain/reseller
admin privileges on this particular NetSapiens deployment; see Known Gaps.

## API Reference

- Public, no login required: https://api.ucaasnetwork.com/ns-api/apidoc/
  (full ns-api reference, including OAuth2, Domain, Reseller, Device,
  Subscriber, and CDR objects)

## Known Gaps

- **Scope is exactly MSPbots' 5 configured endpoints, not the vendor's
  full API surface** — ns-api also covers Callqueue, Agent, Phonenumber,
  Dialplan, Contacts, Presence, Call Queue Report/Stat, real-time Call
  control, and more (per the public docs' own object list); those are out
  of scope here.
- **`oitvoip_get_domains` and `oitvoip_get_resellers` could not be fully
  live-verified with real data** — the provided test account authenticates
  successfully (proving the OAuth2 flow and this implementation are
  correct) but is scoped as a subscriber-level "Office Manager" role,
  which NetSapiens rejects for these two admin-level objects with `401
  Invalid Scope [APP001]`. This is a credential-privilege limitation of
  the specific test account, not a bug in this server — `oitvoip_get_
  subscribers`, `oitvoip_get_devices`, and `oitvoip_get_cdr2` all
  succeeded with real data using the exact same access token from the
  exact same login.
- **CDR date range fields (`start_date`/`end_date`) are unvalidated
  strings** — passed through to the vendor as-is in `YYYY-MM-DD HH:MM:SS`
  format, matching MSPbots' own stored usage; no client-side date parsing
  is performed.