kvc-mcp 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kvc_mcp-0.1.0/.github/workflows/publish.yml +39 -0
- kvc_mcp-0.1.0/.gitignore +14 -0
- kvc_mcp-0.1.0/LICENSE +21 -0
- kvc_mcp-0.1.0/PKG-INFO +74 -0
- kvc_mcp-0.1.0/PUBLISH.md +16 -0
- kvc_mcp-0.1.0/README.md +48 -0
- kvc_mcp-0.1.0/pyproject.toml +37 -0
- kvc_mcp-0.1.0/server.json +37 -0
- kvc_mcp-0.1.0/src/kvc_mcp/__init__.py +3 -0
- kvc_mcp-0.1.0/src/kvc_mcp/server.py +210 -0
- kvc_mcp-0.1.0/tests/test_server.py +87 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
id-token: write
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
pypi:
|
|
14
|
+
name: Build + Publish to PyPI (trusted publisher)
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
environment: pypi
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
- uses: astral-sh/setup-uv@v6
|
|
20
|
+
- name: Build wheel + sdist
|
|
21
|
+
run: uv build
|
|
22
|
+
- name: Publish to PyPI
|
|
23
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
24
|
+
|
|
25
|
+
registry:
|
|
26
|
+
name: Publish server.json to MCP Registry
|
|
27
|
+
runs-on: ubuntu-latest
|
|
28
|
+
needs: pypi
|
|
29
|
+
steps:
|
|
30
|
+
- uses: actions/checkout@v4
|
|
31
|
+
- name: Install mcp-publisher
|
|
32
|
+
run: |
|
|
33
|
+
curl -sSL https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_linux_amd64.tar.gz \
|
|
34
|
+
| tar -xz -C /usr/local/bin mcp-publisher
|
|
35
|
+
chmod +x /usr/local/bin/mcp-publisher
|
|
36
|
+
- name: Login (GitHub OIDC)
|
|
37
|
+
run: mcp-publisher login github-oidc
|
|
38
|
+
- name: Publish
|
|
39
|
+
run: mcp-publisher publish
|
kvc_mcp-0.1.0/.gitignore
ADDED
kvc_mcp-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Krystal Unity
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
kvc_mcp-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kvc-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server for Krystal Voice Caller — let AI agents manage your voice tenant, drive scripts, place test calls, and harvest captures.
|
|
5
|
+
Project-URL: Homepage, https://krystalunity.com/voice/krystal-caller
|
|
6
|
+
Project-URL: Documentation, https://github.com/KrystalUnity/kvc-mcp#readme
|
|
7
|
+
Project-URL: Repository, https://github.com/KrystalUnity/kvc-mcp
|
|
8
|
+
Project-URL: Issues, https://github.com/KrystalUnity/kvc-mcp/issues
|
|
9
|
+
Author-email: Krystal Unity <support@krystalunity.com>
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: ai-agent,claude,cursor,krystal-voice-caller,mcp,openai-realtime,outbound,reception,twilio,voice
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Communications :: Telephony
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Requires-Dist: httpx>=0.27
|
|
24
|
+
Requires-Dist: mcp>=1.0.0
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# kvc-mcp
|
|
28
|
+
|
|
29
|
+
MCP access is included with every Krystal Voice Caller tier. Use this package to manage a tenant from Claude, Cursor, or any MCP-aware agent.
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install kvc-mcp
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Configure
|
|
38
|
+
|
|
39
|
+
Create or rotate an API token in the Krystal Voice Caller dashboard, then set:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
export KVC_API_TOKEN="kvc_token_..."
|
|
43
|
+
export KVC_BASE_URL="https://krystalunity.com/api/admin/kvc"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Cursor example:
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"mcpServers": {
|
|
51
|
+
"krystal-voice-caller": {
|
|
52
|
+
"command": "kvc-mcp",
|
|
53
|
+
"env": {
|
|
54
|
+
"KVC_API_TOKEN": "kvc_token_..."
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Tools
|
|
62
|
+
|
|
63
|
+
The server exposes tenant config, DNC, call history, reception captures, digest send-now, Script Author draft chat, contact upload, outbound captures, and test-call tools. Product availability is enforced by the Krystal Voice Caller API, so Reception-only tenants get inbound/reception tools and Bundle/Premium tenants get outbound Email Hunter tools.
|
|
64
|
+
|
|
65
|
+
Script approval is not exposed through MCP. Approval stays admin-only in the web UI.
|
|
66
|
+
|
|
67
|
+
## Publish
|
|
68
|
+
|
|
69
|
+
Operator-only steps:
|
|
70
|
+
|
|
71
|
+
1. `cd packages/kvc-mcp && uv build`
|
|
72
|
+
2. `uv publish --username __token__ --password "$PYPI_TOKEN"`
|
|
73
|
+
3. Submit an MCP Registry entry named `io.github.KrystalUnity/kvc-mcp` pointing at the PyPI package.
|
|
74
|
+
4. Verify from a fresh venv with `pip install kvc-mcp && kvc-mcp --help`.
|
kvc_mcp-0.1.0/PUBLISH.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Publishing kvc-mcp
|
|
2
|
+
|
|
3
|
+
Publishing is operator-only.
|
|
4
|
+
|
|
5
|
+
1. `cd packages/kvc-mcp && uv build`
|
|
6
|
+
2. `uv publish --username __token__ --password "$PYPI_TOKEN"`
|
|
7
|
+
3. After PyPI has `kvc-mcp`, submit to the MCP Registry:
|
|
8
|
+
- registry name: `io.github.KrystalUnity/kvc-mcp`
|
|
9
|
+
- package: PyPI `kvc-mcp`
|
|
10
|
+
- source: `https://github.com/KrystalUnity/kvc-mcp`
|
|
11
|
+
4. Verify in a fresh venv:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install kvc-mcp
|
|
15
|
+
kvc-mcp --help
|
|
16
|
+
```
|
kvc_mcp-0.1.0/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# kvc-mcp
|
|
2
|
+
|
|
3
|
+
MCP access is included with every Krystal Voice Caller tier. Use this package to manage a tenant from Claude, Cursor, or any MCP-aware agent.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install kvc-mcp
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Configure
|
|
12
|
+
|
|
13
|
+
Create or rotate an API token in the Krystal Voice Caller dashboard, then set:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
export KVC_API_TOKEN="kvc_token_..."
|
|
17
|
+
export KVC_BASE_URL="https://krystalunity.com/api/admin/kvc"
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Cursor example:
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"mcpServers": {
|
|
25
|
+
"krystal-voice-caller": {
|
|
26
|
+
"command": "kvc-mcp",
|
|
27
|
+
"env": {
|
|
28
|
+
"KVC_API_TOKEN": "kvc_token_..."
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Tools
|
|
36
|
+
|
|
37
|
+
The server exposes tenant config, DNC, call history, reception captures, digest send-now, Script Author draft chat, contact upload, outbound captures, and test-call tools. Product availability is enforced by the Krystal Voice Caller API, so Reception-only tenants get inbound/reception tools and Bundle/Premium tenants get outbound Email Hunter tools.
|
|
38
|
+
|
|
39
|
+
Script approval is not exposed through MCP. Approval stays admin-only in the web UI.
|
|
40
|
+
|
|
41
|
+
## Publish
|
|
42
|
+
|
|
43
|
+
Operator-only steps:
|
|
44
|
+
|
|
45
|
+
1. `cd packages/kvc-mcp && uv build`
|
|
46
|
+
2. `uv publish --username __token__ --password "$PYPI_TOKEN"`
|
|
47
|
+
3. Submit an MCP Registry entry named `io.github.KrystalUnity/kvc-mcp` pointing at the PyPI package.
|
|
48
|
+
4. Verify from a fresh venv with `pip install kvc-mcp && kvc-mcp --help`.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "kvc-mcp"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "MCP server for Krystal Voice Caller — let AI agents manage your voice tenant, drive scripts, place test calls, and harvest captures."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
authors = [{ name = "Krystal Unity", email = "support@krystalunity.com" }]
|
|
9
|
+
keywords = ["mcp", "krystal-voice-caller", "voice", "twilio", "openai-realtime", "ai-agent", "reception", "outbound", "claude", "cursor"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 4 - Beta",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"License :: OSI Approved :: MIT License",
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Programming Language :: Python :: 3.10",
|
|
16
|
+
"Programming Language :: Python :: 3.11",
|
|
17
|
+
"Programming Language :: Python :: 3.12",
|
|
18
|
+
"Topic :: Communications :: Telephony",
|
|
19
|
+
"Topic :: Software Development :: Libraries",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"httpx>=0.27",
|
|
23
|
+
"mcp>=1.0.0",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.scripts]
|
|
27
|
+
kvc-mcp = "kvc_mcp.server:main"
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://krystalunity.com/voice/krystal-caller"
|
|
31
|
+
Documentation = "https://github.com/KrystalUnity/kvc-mcp#readme"
|
|
32
|
+
Repository = "https://github.com/KrystalUnity/kvc-mcp"
|
|
33
|
+
Issues = "https://github.com/KrystalUnity/kvc-mcp/issues"
|
|
34
|
+
|
|
35
|
+
[build-system]
|
|
36
|
+
requires = ["hatchling>=1.18.0"]
|
|
37
|
+
build-backend = "hatchling.build"
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
+
"name": "io.github.KrystalUnity/kvc-mcp",
|
|
4
|
+
"title": "Krystal Voice Caller",
|
|
5
|
+
"description": "MCP server for Krystal Voice Caller — drive your voice tenant from Claude, Cursor, or any MCP-aware agent. Manage tenant config, DNC, call history, Script Author drafts, contact uploads, test calls, and reception captures.",
|
|
6
|
+
"websiteUrl": "https://krystalunity.com/voice/krystal-caller",
|
|
7
|
+
"repository": {
|
|
8
|
+
"url": "https://github.com/KrystalUnity/kvc-mcp",
|
|
9
|
+
"source": "github"
|
|
10
|
+
},
|
|
11
|
+
"version": "0.1.0",
|
|
12
|
+
"packages": [
|
|
13
|
+
{
|
|
14
|
+
"registryType": "pypi",
|
|
15
|
+
"registryBaseUrl": "https://pypi.org",
|
|
16
|
+
"identifier": "kvc-mcp",
|
|
17
|
+
"version": "0.1.0",
|
|
18
|
+
"transport": {
|
|
19
|
+
"type": "stdio"
|
|
20
|
+
},
|
|
21
|
+
"environmentVariables": [
|
|
22
|
+
{
|
|
23
|
+
"name": "KVC_API_TOKEN",
|
|
24
|
+
"description": "Bearer token starting with kvc_token_. Generate at https://krystalunity.com/voice/krystal-caller/dashboard/api-tokens",
|
|
25
|
+
"isRequired": true,
|
|
26
|
+
"isSecret": true
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"name": "KVC_BASE_URL",
|
|
30
|
+
"description": "API base URL (default https://krystalunity.com/api/admin/kvc).",
|
|
31
|
+
"isRequired": false,
|
|
32
|
+
"default": "https://krystalunity.com/api/admin/kvc"
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""Krystal Voice Caller MCP Server.
|
|
2
|
+
|
|
3
|
+
Use these tools to manage a Krystal Voice Caller tenant: view config, manage
|
|
4
|
+
DNC, browse call history, author scripts, place test calls, and upload
|
|
5
|
+
outbound contacts. Script approval is intentionally not exposed via MCP; that
|
|
6
|
+
admin-only action stays in the web UI.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
import httpx
|
|
17
|
+
from mcp.server.fastmcp import FastMCP
|
|
18
|
+
|
|
19
|
+
API_TOKEN = os.getenv("KVC_API_TOKEN", "")
|
|
20
|
+
BASE_URL = os.getenv("KVC_BASE_URL", "https://krystalunity.com/api/admin/kvc").rstrip("/")
|
|
21
|
+
TIMEOUT = float(os.getenv("KVC_TIMEOUT", "30"))
|
|
22
|
+
BILLING_URL = "https://krystalunity.com/voice/krystal-caller/dashboard/billing"
|
|
23
|
+
TOKEN_URL = "https://krystalunity.com/voice/krystal-caller/dashboard/api-tokens"
|
|
24
|
+
|
|
25
|
+
mcp = FastMCP(
|
|
26
|
+
"Krystal Voice Caller",
|
|
27
|
+
instructions=(
|
|
28
|
+
"Use these tools to manage a Krystal Voice Caller tenant - view config, "
|
|
29
|
+
"manage DNC, browse call history, author scripts, place test calls, "
|
|
30
|
+
"and upload outbound contacts."
|
|
31
|
+
),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
UPDATE_WHITELIST = {"agent_name", "business_hours", "digest_recipient_email", "digest_hour_local"}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _headers() -> dict[str, str]:
|
|
38
|
+
return {"Authorization": f"Bearer {API_TOKEN}", "Accept": "application/json"}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _client() -> httpx.AsyncClient:
|
|
42
|
+
return httpx.AsyncClient(timeout=TIMEOUT)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _tenant_id() -> str:
|
|
46
|
+
token = API_TOKEN.strip()
|
|
47
|
+
if not token:
|
|
48
|
+
raise RuntimeError("KVC_API_TOKEN is required")
|
|
49
|
+
return os.getenv("KVC_TENANT_ID", "me")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
async def _request(method: str, path: str, **kwargs: Any) -> dict[str, Any]:
|
|
53
|
+
async with _client() as client:
|
|
54
|
+
response = await client.request(method, f"{BASE_URL}{path}", headers=_headers(), **kwargs)
|
|
55
|
+
response.raise_for_status()
|
|
56
|
+
return response.json()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _format_error(exc: Exception) -> str:
|
|
60
|
+
if isinstance(exc, httpx.HTTPStatusError):
|
|
61
|
+
status = exc.response.status_code
|
|
62
|
+
if status == 401:
|
|
63
|
+
return f"Authentication failed. Check your KVC_API_TOKEN. Generate a new one at {TOKEN_URL}"
|
|
64
|
+
if status == 403:
|
|
65
|
+
product = "required"
|
|
66
|
+
try:
|
|
67
|
+
detail = exc.response.json().get("detail", {})
|
|
68
|
+
if isinstance(detail, dict):
|
|
69
|
+
product = str(detail.get("product") or product)
|
|
70
|
+
except Exception:
|
|
71
|
+
pass
|
|
72
|
+
return f"This tool requires the {product} product. Upgrade at {BILLING_URL}"
|
|
73
|
+
if status == 429:
|
|
74
|
+
retry = exc.response.headers.get("Retry-After", "?")
|
|
75
|
+
return f"Rate limited. Retry after {retry}s."
|
|
76
|
+
try:
|
|
77
|
+
detail = exc.response.json().get("detail", exc.response.text)
|
|
78
|
+
except Exception:
|
|
79
|
+
detail = exc.response.text
|
|
80
|
+
return f"API error {status}: {detail}"
|
|
81
|
+
return f"Request failed: {exc}"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _json(data: Any) -> str:
|
|
85
|
+
return json.dumps(data, indent=2, default=str)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@mcp.tool()
|
|
89
|
+
async def kvc_get_tenant() -> str:
|
|
90
|
+
"""Return current tenant config."""
|
|
91
|
+
try:
|
|
92
|
+
return _json(await _request("GET", f"/{_tenant_id()}"))
|
|
93
|
+
except Exception as exc:
|
|
94
|
+
return _format_error(exc)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@mcp.tool()
|
|
98
|
+
async def kvc_update_tenant(updates: dict[str, Any]) -> str:
|
|
99
|
+
"""Patch safe tenant config fields."""
|
|
100
|
+
disallowed = sorted(set(updates) - UPDATE_WHITELIST)
|
|
101
|
+
if disallowed:
|
|
102
|
+
return f"These fields are not allowed through MCP: {', '.join(disallowed)}"
|
|
103
|
+
try:
|
|
104
|
+
return _json(await _request("PATCH", f"/{_tenant_id()}", json=updates))
|
|
105
|
+
except Exception as exc:
|
|
106
|
+
return _format_error(exc)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@mcp.tool()
|
|
110
|
+
async def kvc_list_dnc() -> str:
|
|
111
|
+
"""List tenant DNC entries."""
|
|
112
|
+
try:
|
|
113
|
+
return _json(await _request("GET", f"/{_tenant_id()}/dnc"))
|
|
114
|
+
except Exception as exc:
|
|
115
|
+
return _format_error(exc)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@mcp.tool()
|
|
119
|
+
async def kvc_add_dnc(phone: str, reason: str) -> str:
|
|
120
|
+
"""Add an AU E.164 phone number to tenant DNC."""
|
|
121
|
+
try:
|
|
122
|
+
return _json(await _request("POST", f"/{_tenant_id()}/dnc", json={"phone": phone, "reason": reason}))
|
|
123
|
+
except Exception as exc:
|
|
124
|
+
return _format_error(exc)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@mcp.tool()
|
|
128
|
+
async def kvc_call_history(limit: int = 50, kind: str | None = None) -> str:
|
|
129
|
+
"""Return recent call outcomes."""
|
|
130
|
+
try:
|
|
131
|
+
return _json(await _request("GET", f"/{_tenant_id()}/call-history", params={"limit": limit, "kind": kind}))
|
|
132
|
+
except Exception as exc:
|
|
133
|
+
return _format_error(exc)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@mcp.tool()
|
|
137
|
+
async def kvc_after_hours_captures(limit: int = 20) -> str:
|
|
138
|
+
"""Return recent reception captures."""
|
|
139
|
+
try:
|
|
140
|
+
return _json(await _request("GET", f"/{_tenant_id()}/captures", params={"limit": limit, "kind": "inbound"}))
|
|
141
|
+
except Exception as exc:
|
|
142
|
+
return _format_error(exc)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@mcp.tool()
|
|
146
|
+
async def kvc_send_digest_now() -> str:
|
|
147
|
+
"""Trigger an immediate digest email send."""
|
|
148
|
+
try:
|
|
149
|
+
return _json(await _request("POST", f"/{_tenant_id()}/digest/send-now"))
|
|
150
|
+
except Exception as exc:
|
|
151
|
+
return _format_error(exc)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@mcp.tool()
|
|
155
|
+
async def kvc_create_script_draft(prompt: str) -> str:
|
|
156
|
+
"""Start a Script Author draft."""
|
|
157
|
+
try:
|
|
158
|
+
data = await _request("POST", f"/{_tenant_id()}/scripts/draft")
|
|
159
|
+
if prompt.strip():
|
|
160
|
+
data = await _request("POST", f"/{_tenant_id()}/scripts/draft/{data['draft_id']}/message", json={"message": prompt})
|
|
161
|
+
return _json(data)
|
|
162
|
+
except Exception as exc:
|
|
163
|
+
return _format_error(exc)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@mcp.tool()
|
|
167
|
+
async def kvc_chat_script_draft(draft_id: int, message: str) -> str:
|
|
168
|
+
"""Send a message to an active Script Author draft."""
|
|
169
|
+
try:
|
|
170
|
+
return _json(await _request("POST", f"/{_tenant_id()}/scripts/draft/{draft_id}/message", json={"message": message}))
|
|
171
|
+
except Exception as exc:
|
|
172
|
+
return _format_error(exc)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@mcp.tool()
|
|
176
|
+
async def kvc_upload_contacts(csv_content: str) -> str:
|
|
177
|
+
"""Upload CSV contacts for outbound Email Hunter."""
|
|
178
|
+
try:
|
|
179
|
+
files = {"file": ("contacts.csv", csv_content.encode("utf-8"), "text/csv")}
|
|
180
|
+
return _json(await _request("POST", f"/{_tenant_id()}/contacts/upload", files=files))
|
|
181
|
+
except Exception as exc:
|
|
182
|
+
return _format_error(exc)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@mcp.tool()
|
|
186
|
+
async def kvc_list_outbound_captures(limit: int = 50) -> str:
|
|
187
|
+
"""List captures from outbound Email Hunter calls."""
|
|
188
|
+
try:
|
|
189
|
+
return _json(await _request("GET", f"/{_tenant_id()}/captures", params={"limit": limit, "kind": "outbound"}))
|
|
190
|
+
except Exception as exc:
|
|
191
|
+
return _format_error(exc)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@mcp.tool()
|
|
195
|
+
async def kvc_place_test_call(target_phone: str) -> str:
|
|
196
|
+
"""Place a test outbound roleplay call."""
|
|
197
|
+
try:
|
|
198
|
+
return _json(await _request("POST", f"/{_tenant_id()}/test-call", json={"target_phone": target_phone}))
|
|
199
|
+
except Exception as exc:
|
|
200
|
+
return _format_error(exc)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def main() -> None:
|
|
204
|
+
parser = argparse.ArgumentParser(description="Run the Krystal Voice Caller MCP server.")
|
|
205
|
+
parser.parse_args()
|
|
206
|
+
mcp.run()
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
if __name__ == "__main__":
|
|
210
|
+
main()
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
PACKAGE_SRC = Path(__file__).resolve().parents[1] / "src"
|
|
12
|
+
if str(PACKAGE_SRC) not in sys.path:
|
|
13
|
+
sys.path.insert(0, str(PACKAGE_SRC))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _reload_server(monkeypatch):
|
|
17
|
+
monkeypatch.setenv("KVC_API_TOKEN", "kvc_token_ABCDEFGHIJKLMNOPQRSTUVWXYZ234567")
|
|
18
|
+
monkeypatch.setenv("KVC_BASE_URL", "https://krystalunity.com/api/admin/kvc")
|
|
19
|
+
sys.modules.pop("kvc_mcp.server", None)
|
|
20
|
+
return importlib.import_module("kvc_mcp.server")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.mark.asyncio
|
|
24
|
+
async def test_mcp_tool_calls_send_bearer_header(monkeypatch):
|
|
25
|
+
server = _reload_server(monkeypatch)
|
|
26
|
+
seen: dict[str, object] = {}
|
|
27
|
+
|
|
28
|
+
async def handler(request: httpx.Request) -> httpx.Response:
|
|
29
|
+
seen["authorization"] = request.headers.get("authorization")
|
|
30
|
+
seen["url"] = str(request.url)
|
|
31
|
+
return httpx.Response(200, json={"tenant_id": "tenant-a", "company_name": "Acme"})
|
|
32
|
+
|
|
33
|
+
monkeypatch.setattr(server, "_tenant_id", lambda: "tenant-a")
|
|
34
|
+
monkeypatch.setattr(server, "_client", lambda: httpx.AsyncClient(transport=httpx.MockTransport(handler)))
|
|
35
|
+
|
|
36
|
+
payload = await server.kvc_get_tenant()
|
|
37
|
+
|
|
38
|
+
assert seen["authorization"] == "Bearer kvc_token_ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"
|
|
39
|
+
assert seen["url"] == "https://krystalunity.com/api/admin/kvc/tenant-a"
|
|
40
|
+
assert json.loads(payload)["tenant_id"] == "tenant-a"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_mcp_tool_403_returns_human_readable_capability_message(monkeypatch):
|
|
44
|
+
server = _reload_server(monkeypatch)
|
|
45
|
+
request = httpx.Request("GET", "https://example.test")
|
|
46
|
+
response = httpx.Response(
|
|
47
|
+
403,
|
|
48
|
+
request=request,
|
|
49
|
+
json={"detail": {"code": "product_required", "product": "email_hunter"}},
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
message = server._format_error(httpx.HTTPStatusError("forbidden", request=request, response=response))
|
|
53
|
+
|
|
54
|
+
assert "requires the email_hunter product" in message
|
|
55
|
+
assert "upgrade" in message.lower()
|
|
56
|
+
assert "https://krystalunity.com/voice/krystal-caller/dashboard/billing" in message
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_registered_tools_include_all_stream_l_tools(monkeypatch):
|
|
60
|
+
server = _reload_server(monkeypatch)
|
|
61
|
+
|
|
62
|
+
tool_names = {tool.name for tool in server.mcp._tool_manager.list_tools()}
|
|
63
|
+
|
|
64
|
+
assert {
|
|
65
|
+
"kvc_get_tenant",
|
|
66
|
+
"kvc_update_tenant",
|
|
67
|
+
"kvc_list_dnc",
|
|
68
|
+
"kvc_add_dnc",
|
|
69
|
+
"kvc_call_history",
|
|
70
|
+
"kvc_after_hours_captures",
|
|
71
|
+
"kvc_send_digest_now",
|
|
72
|
+
"kvc_create_script_draft",
|
|
73
|
+
"kvc_chat_script_draft",
|
|
74
|
+
"kvc_upload_contacts",
|
|
75
|
+
"kvc_list_outbound_captures",
|
|
76
|
+
"kvc_place_test_call",
|
|
77
|
+
}.issubset(tool_names)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@pytest.mark.asyncio
|
|
81
|
+
async def test_kvc_update_tenant_rejects_non_whitelisted_fields(monkeypatch):
|
|
82
|
+
server = _reload_server(monkeypatch)
|
|
83
|
+
|
|
84
|
+
result = await server.kvc_update_tenant({"agent_name": "Sam", "enabled_products": {"email_hunter": False}})
|
|
85
|
+
|
|
86
|
+
assert "not allowed" in result
|
|
87
|
+
assert "enabled_products" in result
|