geniete-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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Genie, Inc.
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.
@@ -0,0 +1,61 @@
1
+ Metadata-Version: 2.4
2
+ Name: geniete-mcp
3
+ Version: 0.1.0
4
+ Summary: GenieTé MCP server — screens, voice, and channels tools for AI agents
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Keywords: mcp,agents,geniete,tts,pubsub,screens
8
+ Author: Genie, Inc.
9
+ Author-email: renat@geniete.com
10
+ Requires-Python: >=3.10
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Requires-Dist: httpx (>=0.27.0)
16
+ Requires-Dist: mcp (>=1.0.0)
17
+ Project-URL: Company, https://www.genieinc.com/
18
+ Project-URL: Homepage, https://www.geniete.com/
19
+ Description-Content-Type: text/markdown
20
+
21
+ # geniete-mcp
22
+
23
+ One MCP server for the whole [GenieTé](https://www.geniete.com/) suite —
24
+ give an AI agent a live screen, a voice, and a realtime channel with one
25
+ install. Tools appear only for the services you configure.
26
+
27
+ ```bash
28
+ uv tool install geniete-mcp # or: pip install geniete-mcp
29
+ ```
30
+
31
+ ```json
32
+ {
33
+ "mcpServers": {
34
+ "geniete": {
35
+ "command": "geniete-mcp",
36
+ "env": {
37
+ "AGENTSCREEN_API_KEY": "as_key_...",
38
+ "AGENTSCREEN_SCREEN_ID": "scr_...",
39
+ "VOICE_API_KEY": "vk_...",
40
+ "VOICE_SPEAKER_ID": "spk_...",
41
+ "CHANNELS_TOKEN": "tok_...",
42
+ "CHANNELS_CHANNEL_ID": "chan_..."
43
+ }
44
+ }
45
+ }
46
+ }
47
+ ```
48
+
49
+ Configure any subset — each pair of env vars switches on its tool group:
50
+
51
+ | Tools | Service |
52
+ |-------|---------|
53
+ | `screen_message` `screen_metric` `screen_terminal` `screen_clear` `screen_link` | [Screens](https://screens.geniete.com/docs/screens/) |
54
+ | `voice_say` `voice_speak_url` `voice_listen_link` `voice_voices` | [Voice](https://voice.geniete.com/docs/voice/) |
55
+ | `channel_publish` `channel_ticket` | [Channels](https://channels.geniete.com/docs/channels/) |
56
+
57
+ Keys come from each service's console; one GenieTé account and one
58
+ shared credit balance cover all of them (account.geniete.com).
59
+
60
+ MIT © Genie, Inc.
61
+
@@ -0,0 +1,40 @@
1
+ # geniete-mcp
2
+
3
+ One MCP server for the whole [GenieTé](https://www.geniete.com/) suite —
4
+ give an AI agent a live screen, a voice, and a realtime channel with one
5
+ install. Tools appear only for the services you configure.
6
+
7
+ ```bash
8
+ uv tool install geniete-mcp # or: pip install geniete-mcp
9
+ ```
10
+
11
+ ```json
12
+ {
13
+ "mcpServers": {
14
+ "geniete": {
15
+ "command": "geniete-mcp",
16
+ "env": {
17
+ "AGENTSCREEN_API_KEY": "as_key_...",
18
+ "AGENTSCREEN_SCREEN_ID": "scr_...",
19
+ "VOICE_API_KEY": "vk_...",
20
+ "VOICE_SPEAKER_ID": "spk_...",
21
+ "CHANNELS_TOKEN": "tok_...",
22
+ "CHANNELS_CHANNEL_ID": "chan_..."
23
+ }
24
+ }
25
+ }
26
+ }
27
+ ```
28
+
29
+ Configure any subset — each pair of env vars switches on its tool group:
30
+
31
+ | Tools | Service |
32
+ |-------|---------|
33
+ | `screen_message` `screen_metric` `screen_terminal` `screen_clear` `screen_link` | [Screens](https://screens.geniete.com/docs/screens/) |
34
+ | `voice_say` `voice_speak_url` `voice_listen_link` `voice_voices` | [Voice](https://voice.geniete.com/docs/voice/) |
35
+ | `channel_publish` `channel_ticket` | [Channels](https://channels.geniete.com/docs/channels/) |
36
+
37
+ Keys come from each service's console; one GenieTé account and one
38
+ shared credit balance cover all of them (account.geniete.com).
39
+
40
+ MIT © Genie, Inc.
@@ -0,0 +1,49 @@
1
+ # Publishes to PyPI as "geniete-mcp"
2
+ # Install: uv tool install geniete-mcp
3
+ #
4
+ # Public package: NO GenieForge dependencies. One MCP server for the whole
5
+ # GenieTé suite — tools appear only for the services you configure via env.
6
+ [project]
7
+ name = "geniete-mcp"
8
+ version = "0.1.0"
9
+ description = "GenieTé MCP server — screens, voice, and channels tools for AI agents"
10
+ readme = "README.md"
11
+ requires-python = ">=3.10"
12
+ license = { text = "MIT" }
13
+ keywords = ["mcp", "agents", "geniete", "tts", "pubsub", "screens"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ ]
20
+ dependencies = [
21
+ "mcp>=1.0.0",
22
+ "httpx>=0.27.0",
23
+ ]
24
+
25
+ [project.scripts]
26
+ geniete-mcp = "geniete_mcp.server:main"
27
+
28
+ [project.urls]
29
+ Homepage = "https://www.geniete.com/"
30
+ Company = "https://www.genieinc.com/"
31
+
32
+ [build-system]
33
+ requires = ["poetry-core"]
34
+ build-backend = "poetry.core.masonry.api"
35
+
36
+ [tool.poetry]
37
+ name = "geniete-mcp"
38
+ version = "0.1.0"
39
+ description = "GenieTé MCP server"
40
+ authors = ["Genie, Inc. <renat@geniete.com>"]
41
+ packages = [{ include = "geniete_mcp", from = "src" }]
42
+
43
+ [tool.poetry.dependencies]
44
+ python = ">=3.10"
45
+ mcp = ">=1.0.0"
46
+ httpx = ">=0.27.0"
47
+
48
+ [tool.poetry.scripts]
49
+ geniete-mcp = "geniete_mcp.server:main"
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,237 @@
1
+ """
2
+ GenieTé MCP server — one server, the whole suite. Tools appear only for
3
+ the services you configure:
4
+
5
+ {
6
+ "mcpServers": {
7
+ "geniete": {
8
+ "command": "geniete-mcp",
9
+ "env": {
10
+ "AGENTSCREEN_API_KEY": "as_key_...", // screens tools
11
+ "AGENTSCREEN_SCREEN_ID": "scr_...",
12
+ "VOICE_API_KEY": "vk_...", // voice tools
13
+ "VOICE_SPEAKER_ID": "spk_...",
14
+ "CHANNELS_TOKEN": "tok_...", // channels tools
15
+ "CHANNELS_CHANNEL_ID": "chan_..."
16
+ }
17
+ }
18
+ }
19
+ }
20
+
21
+ Tools (per configured service):
22
+ screen_message / screen_metric / screen_terminal / screen_clear / screen_link
23
+ voice_say / voice_speak_url / voice_listen_link / voice_voices
24
+ channel_publish / channel_ticket
25
+ """
26
+ from __future__ import annotations
27
+
28
+ import asyncio
29
+ import json
30
+ import os
31
+ import uuid
32
+ from typing import Any
33
+
34
+ import httpx
35
+ from mcp.server import Server
36
+ from mcp.server.stdio import stdio_server
37
+ from mcp.types import TextContent, Tool
38
+
39
+ _TONES = ("info", "success", "warning", "error")
40
+
41
+
42
+ def _env(name: str, default: str = "") -> str:
43
+ return os.environ.get(name, default).strip()
44
+
45
+
46
+ class Config:
47
+ """Read once; each service group is on when its env pair is set."""
48
+
49
+ def __init__(self) -> None:
50
+ self.screens_key = _env("AGENTSCREEN_API_KEY")
51
+ self.screens_id = _env("AGENTSCREEN_SCREEN_ID")
52
+ self.screens_base = _env("AGENTSCREEN_BASE_URL", "https://screens.geniete.com").rstrip("/")
53
+ self.voice_key = _env("VOICE_API_KEY")
54
+ self.voice_id = _env("VOICE_SPEAKER_ID")
55
+ self.voice_base = _env("VOICE_BASE_URL", "https://voice.geniete.com").rstrip("/")
56
+ self.channels_token = _env("CHANNELS_TOKEN")
57
+ self.channels_id = _env("CHANNELS_CHANNEL_ID")
58
+ self.channels_base = _env("CHANNELS_BASE_URL", "https://channels.geniete.com").rstrip("/")
59
+
60
+ @property
61
+ def screens_on(self) -> bool:
62
+ return bool(self.screens_key and self.screens_id)
63
+
64
+ @property
65
+ def voice_on(self) -> bool:
66
+ return bool(self.voice_key and self.voice_id)
67
+
68
+ @property
69
+ def channels_on(self) -> bool:
70
+ return bool(self.channels_token and self.channels_id)
71
+
72
+
73
+ def available_tools(cfg: Config) -> list[Tool]:
74
+ tools: list[Tool] = []
75
+ if cfg.screens_on:
76
+ tools += [
77
+ Tool(name="screen_message",
78
+ description="Show a status card on the live screen (title, body, tone info|success|warning|error).",
79
+ inputSchema={"type": "object", "properties": {
80
+ "body": {"type": "string"}, "title": {"type": "string"},
81
+ "tone": {"type": "string", "enum": list(_TONES)}},
82
+ "required": ["body"]}),
83
+ Tool(name="screen_metric",
84
+ description="Push a named metric tile (updated in place by name).",
85
+ inputSchema={"type": "object", "properties": {
86
+ "name": {"type": "string"}, "value": {"type": ["number", "string"]},
87
+ "unit": {"type": "string"}}, "required": ["name", "value"]}),
88
+ Tool(name="screen_terminal",
89
+ description="Append text to the screen's terminal block (include your own newlines).",
90
+ inputSchema={"type": "object", "properties": {"text": {"type": "string"}},
91
+ "required": ["text"]}),
92
+ Tool(name="screen_clear",
93
+ description="Clear everything currently shown on the screen.",
94
+ inputSchema={"type": "object", "properties": {}}),
95
+ Tool(name="screen_link",
96
+ description="Mint a one-time viewer URL for the screen (share it; previews can't consume it).",
97
+ inputSchema={"type": "object", "properties": {
98
+ "ttl_seconds": {"type": "integer"}}}),
99
+ ]
100
+ if cfg.voice_on:
101
+ tools += [
102
+ Tool(name="voice_say",
103
+ description="Speak text out loud on every open listen page of the speaker (pump mode; nothing returned but the charge).",
104
+ inputSchema={"type": "object", "properties": {
105
+ "text": {"type": "string"}, "voice_id": {"type": "string"}},
106
+ "required": ["text"]}),
107
+ Tool(name="voice_speak_url",
108
+ description="Turn text into speech and return a hosted MP3 link (kept ~1 day, plays anywhere).",
109
+ inputSchema={"type": "object", "properties": {
110
+ "text": {"type": "string"}, "voice_id": {"type": "string"}},
111
+ "required": ["text"]}),
112
+ Tool(name="voice_listen_link",
113
+ description="Mint a standing speaker-page link — open it on any device; every voice_say plays there.",
114
+ inputSchema={"type": "object", "properties": {
115
+ "ttl_seconds": {"type": "integer"}}}),
116
+ Tool(name="voice_voices",
117
+ description="List the available voice ids.",
118
+ inputSchema={"type": "object", "properties": {}}),
119
+ ]
120
+ if cfg.channels_on:
121
+ tools += [
122
+ Tool(name="channel_publish",
123
+ description="Publish a JSON payload to the channel; returns how many subscribers received it.",
124
+ inputSchema={"type": "object", "properties": {
125
+ "payload": {"type": "object"}}, "required": ["payload"]}),
126
+ Tool(name="channel_ticket",
127
+ description="Mint a one-time 30-second subscribe credential for a browser client.",
128
+ inputSchema={"type": "object", "properties": {}}),
129
+ ]
130
+ return tools
131
+
132
+
133
+ # ── HTTP helpers (sync httpx; called via to_thread) ──────────────────
134
+
135
+ def _post(url: str, bearer: str, body: dict, *, transport: Any = None) -> httpx.Response:
136
+ with httpx.Client(timeout=130.0, transport=transport) as c:
137
+ return c.post(url, headers={"Authorization": f"Bearer {bearer}",
138
+ "Content-Type": "application/json"}, json=body)
139
+
140
+
141
+ def _get(url: str, *, transport: Any = None) -> httpx.Response:
142
+ with httpx.Client(timeout=30.0, transport=transport) as c:
143
+ return c.get(url)
144
+
145
+
146
+ def dispatch(cfg: Config, name: str, args: dict, *, transport: Any = None) -> str:
147
+ """Execute one tool call; returns the text result. Raises nothing —
148
+ API refusals come back as readable text (agents retry better on
149
+ words than on tracebacks)."""
150
+ def screens_event(event: dict) -> str:
151
+ r = _post(f"{cfg.screens_base}/public/api/publish", cfg.screens_key,
152
+ {"screen_id": cfg.screens_id, "event": event}, transport=transport)
153
+ return "published" if r.status_code < 400 else f"refused: {r.text[:300]}"
154
+
155
+ if name == "screen_message":
156
+ tone = args.get("tone", "info")
157
+ return screens_event({"type": "content.message", "title": args.get("title", ""),
158
+ "body": str(args.get("body", ""))[:8192],
159
+ "tone": tone if tone in _TONES else "info"})
160
+ if name == "screen_metric":
161
+ return screens_event({"type": "metric.update", "name": str(args["name"])[:100],
162
+ "value": args["value"], "unit": args.get("unit", "")})
163
+ if name == "screen_terminal":
164
+ return screens_event({"type": "terminal.append", "text": str(args["text"])[:32768]})
165
+ if name == "screen_clear":
166
+ return screens_event({"type": "display.clear"})
167
+ if name == "screen_link":
168
+ r = _post(f"{cfg.screens_base}/public/api/screens/{cfg.screens_id}/tokens",
169
+ cfg.screens_key,
170
+ {"type": "one-time", "ttl_seconds": int(args.get("ttl_seconds", 3600))},
171
+ transport=transport)
172
+ return r.json().get("url", "") if r.status_code < 400 else f"refused: {r.text[:300]}"
173
+
174
+ if name in ("voice_say", "voice_speak_url"):
175
+ body = {"speaker_id": cfg.voice_id, "voice_id": args.get("voice_id", "kanika"),
176
+ "text": str(args.get("text", ""))[:4000],
177
+ "request_id": f"mcp_{uuid.uuid4().hex[:16]}"}
178
+ body["deliver" if name == "voice_speak_url" else "return_audio"] = (
179
+ "url" if name == "voice_speak_url" else False)
180
+ r = _post(f"{cfg.voice_base}/public/api/speak", cfg.voice_key, body, transport=transport)
181
+ if r.status_code >= 400:
182
+ return f"refused: {r.text[:300]}"
183
+ d = r.json()
184
+ return d.get("url") or f"spoken ({d.get('charged_credits', '?')} credits)"
185
+ if name == "voice_listen_link":
186
+ body = {"ttl_seconds": int(args["ttl_seconds"])} if args.get("ttl_seconds") else {}
187
+ r = _post(f"{cfg.voice_base}/public/api/speakers/{cfg.voice_id}/listen-links",
188
+ cfg.voice_key, body, transport=transport)
189
+ return r.json().get("url", "") if r.status_code < 400 else f"refused: {r.text[:300]}"
190
+ if name == "voice_voices":
191
+ r = _get(f"{cfg.voice_base}/public/api/voices", transport=transport)
192
+ if r.status_code >= 400:
193
+ return f"refused: {r.text[:300]}"
194
+ return ", ".join(v.get("id", "") for v in r.json().get("voices", []))
195
+
196
+ if name == "channel_publish":
197
+ r = _post(f"{cfg.channels_base}/api/v1/publish/{cfg.channels_id}",
198
+ cfg.channels_token, dict(args.get("payload") or {}), transport=transport)
199
+ if r.status_code >= 400:
200
+ return f"refused: {r.text[:300]}"
201
+ return f"delivered to {r.json().get('receivers', 0)} subscriber(s)"
202
+ if name == "channel_ticket":
203
+ r = _post(f"{cfg.channels_base}/api/v1/channels/{cfg.channels_id}/tickets",
204
+ cfg.channels_token, {}, transport=transport)
205
+ return json.dumps(r.json()) if r.status_code < 400 else f"refused: {r.text[:300]}"
206
+
207
+ return f"unknown tool: {name}"
208
+
209
+
210
+ def build_server(cfg: Config) -> Server:
211
+ server = Server("geniete")
212
+
213
+ @server.list_tools()
214
+ async def list_tools() -> list[Tool]:
215
+ return available_tools(cfg)
216
+
217
+ @server.call_tool()
218
+ async def call_tool(name: str, arguments: dict) -> list[TextContent]:
219
+ text = await asyncio.to_thread(dispatch, cfg, name, arguments or {})
220
+ return [TextContent(type="text", text=text)]
221
+
222
+ return server
223
+
224
+
225
+ def main() -> None:
226
+ cfg = Config()
227
+ server = build_server(cfg)
228
+
229
+ async def run() -> None:
230
+ async with stdio_server() as (read, write):
231
+ await server.run(read, write, server.create_initialization_options())
232
+
233
+ asyncio.run(run())
234
+
235
+
236
+ if __name__ == "__main__":
237
+ main()