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.
@@ -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
@@ -0,0 +1,14 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .venv/
5
+ .env
6
+ dist/
7
+ build/
8
+ .pytest_cache/
9
+ .mypy_cache/
10
+ .ruff_cache/
11
+ .coverage
12
+ htmlcov/
13
+ *.log
14
+ .DS_Store
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`.
@@ -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
+ ```
@@ -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,3 @@
1
+ """Krystal Voice Caller MCP package."""
2
+
3
+ __version__ = "0.1.0"
@@ -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