mcp-server-webos 0.1.0__py3-none-any.whl
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.
- mcp_server_webos-0.1.0.dist-info/METADATA +503 -0
- mcp_server_webos-0.1.0.dist-info/RECORD +36 -0
- mcp_server_webos-0.1.0.dist-info/WHEEL +5 -0
- mcp_server_webos-0.1.0.dist-info/entry_points.txt +2 -0
- mcp_server_webos-0.1.0.dist-info/licenses/LICENSE +21 -0
- mcp_server_webos-0.1.0.dist-info/top_level.txt +1 -0
- webos_mcp/__init__.py +5 -0
- webos_mcp/apps.py +199 -0
- webos_mcp/assumptions.py +440 -0
- webos_mcp/capture.py +77 -0
- webos_mcp/cli.py +466 -0
- webos_mcp/client.py +1087 -0
- webos_mcp/compat.py +188 -0
- webos_mcp/config.py +431 -0
- webos_mcp/discovery.py +69 -0
- webos_mcp/doctor.py +322 -0
- webos_mcp/errors.py +19 -0
- webos_mcp/keystore.py +54 -0
- webos_mcp/logsafe.py +309 -0
- webos_mcp/lookup.py +60 -0
- webos_mcp/probe.py +66 -0
- webos_mcp/ratelimit.py +226 -0
- webos_mcp/redact.py +66 -0
- webos_mcp/registry.py +141 -0
- webos_mcp/remote.py +516 -0
- webos_mcp/server.py +571 -0
- webos_mcp/sim/__init__.py +0 -0
- webos_mcp/sim/fake_tv.py +607 -0
- webos_mcp/sim/fixtures/c5-webos25.json +1221 -0
- webos_mcp/sim/fixtures/c7.json +1007 -0
- webos_mcp/sim/fixtures/g1.json +1378 -0
- webos_mcp/sim/profiles.py +197 -0
- webos_mcp/tasks.py +174 -0
- webos_mcp/tools.py +29 -0
- webos_mcp/tvs.py +64 -0
- webos_mcp/wol.py +73 -0
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mcp-server-webos
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server for LG webOS TVs (SSAP over WebSocket, via aiowebostv) with a one-time pairing CLI
|
|
5
|
+
Author: Nicholas E. Andrade
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/SDNick484/mcp-server-webos
|
|
8
|
+
Project-URL: Issues, https://github.com/SDNick484/mcp-server-webos/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/SDNick484/mcp-server-webos/blob/main/CHANGELOG.md
|
|
10
|
+
Keywords: mcp,mcp-server,lg,webos,lg-tv,oled,home-automation
|
|
11
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
12
|
+
Classifier: Framework :: AsyncIO
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Topic :: Home Automation
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: mcp<3,>=2.3
|
|
24
|
+
Requires-Dist: aiowebostv<0.11,>=0.9.2
|
|
25
|
+
Requires-Dist: typing_extensions>=4.7
|
|
26
|
+
Requires-Dist: pyjwt[crypto]>=2.10
|
|
27
|
+
Requires-Dist: uvicorn>=0.31
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
30
|
+
Requires-Dist: anyio>=4; extra == "dev"
|
|
31
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
32
|
+
Requires-Dist: mypy>=1.11; extra == "dev"
|
|
33
|
+
Requires-Dist: httpx2; extra == "dev"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
# mcp-server-webos
|
|
37
|
+
|
|
38
|
+
An [MCP](https://modelcontextprotocol.io) server for LG webOS TVs: status, power, screen, apps and
|
|
39
|
+
inputs, remote keys, typing into search boxes, YouTube search, playback and volume, from Claude Code, Claude Desktop or the mobile app. It
|
|
40
|
+
talks to each TV directly over LG's SSAP WebSocket protocol, through
|
|
41
|
+
[aiowebostv](https://github.com/home-assistant-libs/aiowebostv) (the library Home Assistant uses),
|
|
42
|
+
with no dependency on Home Assistant.
|
|
43
|
+
|
|
44
|
+
A sibling of [mcp-server-onkyo](https://github.com/SDNick484/mcp-server-onkyo),
|
|
45
|
+
[mcp-server-shieldtv](https://github.com/SDNick484/mcp-server-shieldtv),
|
|
46
|
+
[mcp-server-harmony](https://github.com/SDNick484/mcp-server-harmony) and
|
|
47
|
+
[mcp-server-sofabaton](https://github.com/SDNick484/mcp-server-sofabaton), with the same
|
|
48
|
+
conventions: typed tools with annotations, an `ActionResult` from every action, dry run, `doctor`,
|
|
49
|
+
a wire-level simulator, an assumptions registry, Streamable HTTP behind Cloudflare Access, and an
|
|
50
|
+
OpenRC service for an Alpine LXC.
|
|
51
|
+
|
|
52
|
+
> **Status: alpha, and looking for testers with other LG TVs.** It runs daily on the three TVs below:
|
|
53
|
+
> pairing, status, screen off, sources, the text tools and YouTube search have run on real sets
|
|
54
|
+
> (2026-10-14 to 2026-11-10), and the HTTP service runs behind Cloudflare Access. Wake-on-LAN, keys
|
|
55
|
+
> and media controls have so far run only on the 2025 C5, and other models only against the
|
|
56
|
+
> simulator. [HARDWARE_VALIDATION.md](https://github.com/SDNick484/mcp-server-webos/blob/main/HARDWARE_VALIDATION.md) is the checklist, the table at the end
|
|
57
|
+
> of this README says what has been confirmed, and a
|
|
58
|
+
> [hardware report](https://github.com/SDNick484/mcp-server-webos/issues/new?template=hardware_report.yml)
|
|
59
|
+
> for your TV (working or not) is the most useful thing you can send.
|
|
60
|
+
|
|
61
|
+
## The TVs it targets
|
|
62
|
+
|
|
63
|
+
The owner's three OLEDs, one per generation. Other webOS TVs (2014 onwards) speak the same
|
|
64
|
+
protocol and will probably work, but nothing is promised for them.
|
|
65
|
+
|
|
66
|
+
| Room | Model | Platform | Control port | Notes |
|
|
67
|
+
| --- | --- | --- | --- | --- |
|
|
68
|
+
| Bedroom | OLED65C7P-U (2017 C7) | webOS 3.5 (reports 3.9.3) | 3000 (ws) | no `now_playing` (no media service) |
|
|
69
|
+
| Family Room | OLED77G1PUA (2021 G1) | webOS 6.5 (reports 6.5.3) | 3000 (ws); 3001 (wss) also open | no `now_playing` (no media service) |
|
|
70
|
+
| Office | OLED65C5PUA (2025 C5) | webOS 25 (26 on offer) | 3001 (wss) | webOS 26 changed pairing (see below) |
|
|
71
|
+
|
|
72
|
+
A TV usually sits on one HDMI input from an Onkyo receiver, and **volume belongs to the
|
|
73
|
+
receiver**: when a TV's sound goes out over eARC/ARC, this server refuses to change its volume and
|
|
74
|
+
points at the receiver's own server instead. Going through the TV would bypass the receiver
|
|
75
|
+
server's volume caps (the TV forwards its volume to the receiver over HDMI-CEC). A separate
|
|
76
|
+
home-theater Skill coordinates the servers; [docs/skill-webos.md](https://github.com/SDNick484/mcp-server-webos/blob/main/docs/skill-webos.md) is this
|
|
77
|
+
server's cheat sheet for it.
|
|
78
|
+
|
|
79
|
+
### webOS 26 and pairing
|
|
80
|
+
|
|
81
|
+
In August 2026 LG's webOS 26 firmware started rejecting the signed registration manifest that
|
|
82
|
+
every SSAP client had sent since 2014 (`403 Pairing rejected: blacklisted certificate detected`,
|
|
83
|
+
before any dialog). aiowebostv 0.9.2 switched to an unsigned manifest, which webOS 26 accepts;
|
|
84
|
+
this server requires at least that version. LG has been rolling webOS 26 out to 2025 sets since
|
|
85
|
+
late August, and there is no way back, so HARDWARE_VALIDATION.md captures the C5 on webOS 25
|
|
86
|
+
before it updates.
|
|
87
|
+
|
|
88
|
+
## Install
|
|
89
|
+
|
|
90
|
+
With [uv](https://docs.astral.sh/uv/), nothing to install: `uvx mcp-server-webos ...` fetches it
|
|
91
|
+
into a cached environment and runs it (`uvx mcp-server-webos pair --host <ip> --name <room>`). Or
|
|
92
|
+
in a virtualenv:
|
|
93
|
+
|
|
94
|
+
```sh
|
|
95
|
+
python -m venv ~/.venvs/webos && . ~/.venvs/webos/bin/activate
|
|
96
|
+
pip install mcp-server-webos
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
From source instead (for the newest code, or to run the tests):
|
|
100
|
+
|
|
101
|
+
```sh
|
|
102
|
+
git clone https://github.com/SDNick484/mcp-server-webos.git && cd mcp-server-webos
|
|
103
|
+
python -m venv .venv && . .venv/bin/activate
|
|
104
|
+
pip install -e ".[dev]" && pytest
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Python 3.11 or newer. Every dependency ships a musl wheel, so Alpine needs no compiler.
|
|
108
|
+
|
|
109
|
+
## Pair each TV
|
|
110
|
+
|
|
111
|
+
The TV must be on. On a webOS 3.x set (the C7), turn on **LG Connect Apps** in its network
|
|
112
|
+
settings first. For `webos_set_power on` to work, also turn on the setting that lets the network wake
|
|
113
|
+
it: "Mobile TV On", "TV On With Mobile" or "Turn on via Wi-Fi", depending on the generation.
|
|
114
|
+
|
|
115
|
+
```sh
|
|
116
|
+
mcp-server-webos pair --host 192.168.1.50 --name "Family Room" --alias G1
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The TV shows a pairing request: accept it with the remote (`pair` waits up to 60 s). `pair` reads
|
|
120
|
+
the TV's identity first (its deviceUUID, from the keyless hello), then saves:
|
|
121
|
+
|
|
122
|
+
- the client key, in `keys.json` (0600), by deviceUUID. It is full control of the TV, so it is
|
|
123
|
+
never logged, never in `config.json`, never in a tool result;
|
|
124
|
+
- the TV in `config.json`: name, aliases (your `--alias`es and the model), address, MACs for
|
|
125
|
+
Wake-on-LAN (the connected interface first; `--mac` if they can't be read), model, platform,
|
|
126
|
+
and the TLS certificate's fingerprint.
|
|
127
|
+
|
|
128
|
+
Both live in `~/.config/mcp-server-webos` (or `$WEBOS_CONFIG_DIR`), 0700. A running server
|
|
129
|
+
notices a new key and connects with it, without a restart. Re-running `pair` for the same TV
|
|
130
|
+
updates its entry and keeps your hand edits.
|
|
131
|
+
|
|
132
|
+
```sh
|
|
133
|
+
mcp-server-webos check # connect once to each TV: platform, what works, what doesn't
|
|
134
|
+
mcp-server-webos doctor # layer by layer, read-only; each failure names a validation step
|
|
135
|
+
mcp-server-webos discover # which webOS TVs answer SSDP (TVs that are off don't)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Choose the apps
|
|
139
|
+
|
|
140
|
+
`webos_select_source` only opens apps on the allow-list: a few defaults, plus yours. To see what a TV
|
|
141
|
+
actually has installed, and add from it:
|
|
142
|
+
|
|
143
|
+
```sh
|
|
144
|
+
mcp-server-webos apps --tv "Office TV" # listed by group, with the name webos_select_source will use
|
|
145
|
+
mcp-server-webos apps --tv "Office TV" --add crunchyroll,"Photo & Video"
|
|
146
|
+
mcp-server-webos apps --all-tvs --add-all # every third-party app on any TV
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
| Group | Means | Added by |
|
|
150
|
+
| --- | --- | --- |
|
|
151
|
+
| Allowed | already on the allow-list | |
|
|
152
|
+
| Not allowed yet | third-party apps | `--add NAME` or `--add-all` |
|
|
153
|
+
| LG's own apps | `com.webos.`, `com.palm.`, `com.lge.` ids (W-SYSTEM-APP-IDS) | `--add NAME` only |
|
|
154
|
+
| Excluded | the web browser (it opens any URL), settings, LG's store (it installs apps), inputs (`webos_select_source` handles them), Live TV, service menus | never |
|
|
155
|
+
|
|
156
|
+
It asks the TV over a short session with the stored key and changes nothing on it. New entries go
|
|
157
|
+
into `config.json`'s `"apps"`; restart the server to use them. Run it as the service user on the
|
|
158
|
+
LXC (`su -s /bin/sh mcp-webos -c '…'` with `WEBOS_CONFIG_DIR=/var/lib/mcp-server-webos`), since
|
|
159
|
+
`keys.json` and `config.json` are that user's. There is no tool for this: the model can't widen the
|
|
160
|
+
allow-list.
|
|
161
|
+
|
|
162
|
+
### config.json
|
|
163
|
+
|
|
164
|
+
`pair` writes it; you may edit names, aliases, `max_volume` and `volume_owner`:
|
|
165
|
+
|
|
166
|
+
```json
|
|
167
|
+
{
|
|
168
|
+
"tvs": [
|
|
169
|
+
{"name": "Bedroom", "aliases": ["C7", "OLED65C7P-U"], "host": "192.168.1.52",
|
|
170
|
+
"uuid": "...", "macs": ["a8:23:fe:..."], "model": "OLED65C7P-U", "platform": "3.5.0",
|
|
171
|
+
"max_volume": 30, "volume_owner": "auto"}
|
|
172
|
+
],
|
|
173
|
+
"apps": {"crunchyroll": "crunchyroll.app.id"},
|
|
174
|
+
"max_volume": 40
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
- `max_volume` (0-100, default 40): `webos_set_volume` caps at it, per TV or for all.
|
|
179
|
+
- `volume_owner`: `auto` (from the TV's sound output: external outputs mean the receiver owns
|
|
180
|
+
volume), `tv` (always allow), or `external` (never).
|
|
181
|
+
- `apps`: extra apps for the allow-list, `name: app id` (ids from `webos_list_sources` or a capture).
|
|
182
|
+
- `wol_broadcast` / `wol_port`: where magic packets go (default 255.255.255.255:9).
|
|
183
|
+
- Hosts must be private IPv4 addresses. A name or alias that would mean two TVs is refused.
|
|
184
|
+
|
|
185
|
+
## Use it from Claude
|
|
186
|
+
|
|
187
|
+
Claude Code:
|
|
188
|
+
|
|
189
|
+
```sh
|
|
190
|
+
claude mcp add webos -- uvx mcp-server-webos # or the venv's bin/mcp-server-webos
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Claude Desktop (`claude_desktop_config.json`, then restart Claude Desktop):
|
|
194
|
+
|
|
195
|
+
```json
|
|
196
|
+
{
|
|
197
|
+
"mcpServers": {
|
|
198
|
+
"webos": {"command": "uvx", "args": ["mcp-server-webos"]}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Claude Desktop doesn't start a login shell, so it may not find `uvx` on its `PATH`: if the server
|
|
204
|
+
doesn't appear, put the full path from `which uvx` (`where uvx` on Windows) in `command`. With a
|
|
205
|
+
virtualenv instead, `command` is the full path to its `bin/mcp-server-webos` and `args` is `[]`.
|
|
206
|
+
Both read the TVs and keys that `pair` saved.
|
|
207
|
+
|
|
208
|
+
### Tools
|
|
209
|
+
|
|
210
|
+
Fourteen, on purpose: every tool definition costs context in every client.
|
|
211
|
+
|
|
212
|
+
| Tool | Does | Annotations |
|
|
213
|
+
| --- | --- | --- |
|
|
214
|
+
| `webos_list_tvs` | names and aliases `tv` accepts, model, platform | read-only |
|
|
215
|
+
| `webos_get_status` | power, what is in front, input, now playing, volume, sound output, capabilities and limitations; all TVs when `tv` is omitted | read-only |
|
|
216
|
+
| `webos_set_power` | on (Wake-on-LAN, confirmed) or off (standby); `tv="all"` for off | idempotent |
|
|
217
|
+
| `webos_set_screen` | blank the picture while sound plays, or bring it back | idempotent |
|
|
218
|
+
| `webos_list_sources` | allow-listed apps the TV has installed, and its HDMI inputs | read-only |
|
|
219
|
+
| `webos_select_source` | open an app or switch input, confirmed in front | idempotent |
|
|
220
|
+
| `webos_send_key` | one allow-listed remote key, up to 10 times | not idempotent |
|
|
221
|
+
| `webos_media_control` | play, pause, stop, rewind, fast-forward | not idempotent |
|
|
222
|
+
| `webos_set_volume` | 0-100, capped; refused when the receiver owns volume | idempotent |
|
|
223
|
+
| `webos_set_mute` | mute or unmute; same rule | idempotent |
|
|
224
|
+
| `webos_insert_text` | type into the focused *system* text field (LG's search); `replace` to overwrite it | not idempotent |
|
|
225
|
+
| `webos_delete_text` | delete characters before the cursor in that field | not idempotent |
|
|
226
|
+
| `webos_submit_text` | press the keyboard's Enter in that field (runs the search) | not idempotent |
|
|
227
|
+
| `webos_search_app` | open YouTube straight on its results for a query | idempotent |
|
|
228
|
+
|
|
229
|
+
### Typing and searching
|
|
230
|
+
|
|
231
|
+
Typing a search with `webos_send_key` means walking an on-screen keyboard one arrow at a time. Where
|
|
232
|
+
`webos_get_status` lists `text_input`, `webos_insert_text` types the whole string through the TV's input
|
|
233
|
+
method (IME) service instead:
|
|
234
|
+
|
|
235
|
+
```
|
|
236
|
+
webos_get_status(tv="Office") # capabilities include text_input
|
|
237
|
+
webos_send_key(key="HOME") ... open LG's search # the search box has the cursor
|
|
238
|
+
webos_insert_text(text="lofi beats", tv="Office") # typed, and confirmed by the field's new length
|
|
239
|
+
webos_submit_text(tv="Office") # the results
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
What was seen on the three TVs (2026-11-09; details in HARDWARE_VALIDATION.md step 14):
|
|
243
|
+
|
|
244
|
+
| | Bedroom C7 (3.9.3) | Family Room G1 (6.5.3) | Office C5 (10.3.1) |
|
|
245
|
+
| --- | --- | --- | --- |
|
|
246
|
+
| Typing into LG's search | works, unconfirmed | works, confirmed | works, confirmed |
|
|
247
|
+
| Typing into YouTube's or Netflix's search | refused | refused | refused |
|
|
248
|
+
| `webos_search_app` (YouTube) | refused | works | works |
|
|
249
|
+
|
|
250
|
+
- **Only system text fields take typed text.** YouTube and Netflix draw their own keyboards, and
|
|
251
|
+
the TV answers "ok" to every IME call there (and with nothing focused at all) while typing
|
|
252
|
+
nothing. So each text tool first asks the TV whether a system text field has focus (W-IME-FOCUS)
|
|
253
|
+
and refuses, sending nothing, when none does. The refusal points at `webos_search_app` or `webos_send_key`.
|
|
254
|
+
- **Confirmed where the TV says.** The G1 and C5 report the field's length after every change, so
|
|
255
|
+
the result says whether the text landed; the C7 doesn't, so its results carry an "unconfirmed"
|
|
256
|
+
warning (W-IME-LENGTH).
|
|
257
|
+
- **YouTube search by link, not by typing.** `webos_search_app` launches YouTube with
|
|
258
|
+
`contentTarget=https://www.youtube.com/tv?q=<query>`, the only link shape that showed results
|
|
259
|
+
(W-YOUTUBE-SEARCH). The server builds the link; the model only gives the query. webOS 3.x's
|
|
260
|
+
YouTube ignores links, so it is refused on the C7; webOS 4 and 5 are untested and allowed.
|
|
261
|
+
- Text is one line of printable characters, at most 200 (a query, 100); no newlines (use
|
|
262
|
+
`webos_submit_text`). Typing shares `webos_send_key`'s rate limit.
|
|
263
|
+
|
|
264
|
+
Nothing is `destructiveHint`: turning a TV off is undone by turning it on. Every action returns an
|
|
265
|
+
`ActionResult`, the siblings' shape:
|
|
266
|
+
|
|
267
|
+
```json
|
|
268
|
+
{"tv": "Bedroom", "outcome": "done", "detail": "Bedroom's volume is 15",
|
|
269
|
+
"sent": ["ssap://audio/setVolume 15"], "warnings": []}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
`outcome` is `done`, `unchanged` (already so; nothing sent) or `dry_run`. Failures are tool
|
|
273
|
+
errors whose message says what to do ("Which TV? Several are configured: ...", "Volume on Family
|
|
274
|
+
Room belongs to the receiver ...", "... needs pairing: run pair").
|
|
275
|
+
|
|
276
|
+
Resources: `webos://tvs` (capabilities per TV), `webos://keys` (the key allow-list, and the keys
|
|
277
|
+
left out with the reason for each), `webos://apps` (the app allow-list). One prompt, `watch`.
|
|
278
|
+
|
|
279
|
+
### What the model can't do
|
|
280
|
+
|
|
281
|
+
- **No raw SSAP or luna calls.** The same reasoning as no `adb shell` on the Shield: one generic
|
|
282
|
+
call would reach everything the TV exposes, including its internal settings services.
|
|
283
|
+
- **No URL tools, and text only where decided:** no browser, no "play this URL". The two free-text
|
|
284
|
+
arguments are `webos_insert_text`'s text, which only reaches a focused system text field, and
|
|
285
|
+
`webos_search_app`'s query, which the server puts into a link it builds for an allow-listed app.
|
|
286
|
+
- **Keys are an allow-list.** `POWER` (a toggle), the `IN_START`/`EZ_ADJUST` service menus,
|
|
287
|
+
settings menus, the volume keys (they would bypass the cap) and app shortcuts (they would
|
|
288
|
+
bypass the app allow-list) are left out; `webos://keys` lists why.
|
|
289
|
+
- **Apps are an allow-list** of ids the TV itself lists. Strings from the TV (app titles, input
|
|
290
|
+
labels) are cleaned of control characters, bounded, and treated as data.
|
|
291
|
+
- **Never guess a TV:** with several configured, an action without `tv` is refused and the TVs are
|
|
292
|
+
listed. **Never guess a source:** a name that matches an app and an input is refused.
|
|
293
|
+
- **Screen off uses standbyMode "active" only.** "passive" can't be undone without unplugging the
|
|
294
|
+
TV; it isn't an argument anywhere.
|
|
295
|
+
|
|
296
|
+
[docs/OUT_OF_SCOPE.md](https://github.com/SDNick484/mcp-server-webos/blob/main/docs/OUT_OF_SCOPE.md) lists the rest (picture settings, channels, LG's IP
|
|
297
|
+
control) and why.
|
|
298
|
+
|
|
299
|
+
### Dry run
|
|
300
|
+
|
|
301
|
+
`serve --dry-run` (or `WEBOS_DRY_RUN=1`) reads the TVs but sends nothing that changes anything:
|
|
302
|
+
every action returns `outcome: "dry_run"` with what it would have sent.
|
|
303
|
+
|
|
304
|
+
## How it stays connected
|
|
305
|
+
|
|
306
|
+
Each TV has one long-lived WebSocket (the TV pushes changes over subscriptions) and a supervisor:
|
|
307
|
+
|
|
308
|
+
- **Off:** a TV that is off has no socket. The supervisor retries every ~10 s, so a TV turned on
|
|
309
|
+
by its remote shows up within ~10 s. `webos_get_status` on a stale TV first makes one connect attempt
|
|
310
|
+
of at most 3 s, then answers with `stale`, `as_of` and `error`.
|
|
311
|
+
- **Stale key:** if the TV no longer accepts the stored key (a reset, or a firmware update), it
|
|
312
|
+
wants to show its pairing dialog. The server hangs up at once, marks the TV `needs_pairing` and
|
|
313
|
+
stops trying until `pair` writes a new key. That's one dialog, not one every 10 s.
|
|
314
|
+
- **Moved:** the key is only sent after the TV's hello shows the paired deviceUUID. If another
|
|
315
|
+
device has the address, the server looks for the TV by SSDP and adopts a new address only on a
|
|
316
|
+
deviceUUID match.
|
|
317
|
+
- **Pointer socket refused:** aiowebostv would fail the whole connection; here only the keys stop
|
|
318
|
+
working (see `compat.py`).
|
|
319
|
+
|
|
320
|
+
`compat.py` holds every dependency on aiowebostv internals, with contract tests; that is why the
|
|
321
|
+
dependency is pinned `<0.11`.
|
|
322
|
+
|
|
323
|
+
## HTTP and Cloudflare Access
|
|
324
|
+
|
|
325
|
+
To share one server between Claude Code, Claude Desktop and the mobile app, run it as a service
|
|
326
|
+
and reach it through a Cloudflare Tunnel with Access in front, exactly as the siblings do. Claude's
|
|
327
|
+
connectors reach it from the internet, so the URL is public; Access is what keeps it yours.
|
|
328
|
+
|
|
329
|
+
| Flag | Variable | Default |
|
|
330
|
+
|---|---|---|
|
|
331
|
+
| `--bind` | `MCP_HTTP_BIND` | `127.0.0.1` |
|
|
332
|
+
| `--port` | `MCP_HTTP_PORT` | `8715` (Onkyo 8711, Shield 8712, Harmony 8713, Sofabaton 8714) |
|
|
333
|
+
| `--path` | `MCP_HTTP_PATH` | `/webos/mcp` |
|
|
334
|
+
| `--public-host` | `MCP_PUBLIC_HOSTS` | none: only loopback Host headers pass, so **Cloudflare's requests get 421** |
|
|
335
|
+
| | `CF_ACCESS_TEAM_DOMAIN`, `CF_ACCESS_AUD` | none: no Access checks |
|
|
336
|
+
| | `MCP_ALLOWED_EMAILS` | anyone Access lets in |
|
|
337
|
+
|
|
338
|
+
With Access configured, every request needs a valid `Cf-Access-Jwt-Assertion`; anything else
|
|
339
|
+
gets a plain 403. The server refuses to start on a non-loopback address without Access unless
|
|
340
|
+
you pass `--insecure-no-auth`. `GET /healthz` answers without auth. `remote.py` and `logsafe.py`
|
|
341
|
+
are shared with the sibling servers, from
|
|
342
|
+
[mcp-server-shared](https://github.com/SDNick484/mcp-server-shared).
|
|
343
|
+
|
|
344
|
+
### Setting up Cloudflare
|
|
345
|
+
|
|
346
|
+
This is the setup that works, verified with Claude Code and the Claude app connector on
|
|
347
|
+
2026-10-14. Do the Access application first, so the URL is never public.
|
|
348
|
+
|
|
349
|
+
1. **One Access application for the whole hostname**, shared by every MCP server on it. Zero Trust
|
|
350
|
+
→ Access controls → Applications → Add → Self-hosted:
|
|
351
|
+
- Destination: subdomain `mcp`, your domain, **path empty**. Managed OAuth serves its sign-in
|
|
352
|
+
discovery from the root of the hostname (`/.well-known/...`), so an app limited to
|
|
353
|
+
`/webos` may not cover it; one app also means one sign-in for all the servers.
|
|
354
|
+
- Policy: Allow → Emails → you. Login method: One-time PIN (or your identity provider).
|
|
355
|
+
- Edit the app → **Advanced settings → Managed OAuth: on**. Allowed redirect URIs:
|
|
356
|
+
`https://claude.ai/*`. Turn on **Allow localhost clients** (Claude Code signs in through
|
|
357
|
+
localhost). Access token 15 min, grant session 1-2 weeks.
|
|
358
|
+
- Copy the app's **Application Audience (AUD) tag**.
|
|
359
|
+
- Make sure no *other* Access app covers a path on the same hostname: Access applies the most
|
|
360
|
+
specific match, and a Bypass policy there would let requests through without a token.
|
|
361
|
+
2. **A route on the tunnel.** Networks → Tunnels → the tunnel → Published application routes →
|
|
362
|
+
Add: hostname `mcp.example.com`, path `^/webos/` (a regex), service
|
|
363
|
+
`http://<address>:8715`. Leave the HTTP Host Header override unset: the server checks the
|
|
364
|
+
public Host.
|
|
365
|
+
- The service address is resolved **where cloudflared runs**. If cloudflared is a Docker
|
|
366
|
+
container (the tunnel page lists the replica's hostname as a 12-character hex container ID), `localhost`
|
|
367
|
+
is that container, not the LXC: use the LXC's LAN address or the Docker bridge's
|
|
368
|
+
(`172.17.0.1`, reachable only from containers on that host), and set `MCP_HTTP_BIND` to the same address.
|
|
369
|
+
3. **`/etc/mcp-server-webos/env`** (see `deploy/alpine/env.example`), then
|
|
370
|
+
`rc-service mcp-server-webos restart`:
|
|
371
|
+
```sh
|
|
372
|
+
MCP_HTTP_BIND=192.168.1.10 # e.g. the LXC's address: the one the route points at
|
|
373
|
+
MCP_PUBLIC_HOSTS=mcp.example.com # required: without it every request through Cloudflare gets 421
|
|
374
|
+
CF_ACCESS_TEAM_DOMAIN=<team>.cloudflareaccess.com
|
|
375
|
+
CF_ACCESS_AUD=<the AUD tag from step 1>
|
|
376
|
+
MCP_ALLOWED_EMAILS=<the email you sign in to Access with>
|
|
377
|
+
```
|
|
378
|
+
4. **Test**, then add `https://mcp.example.com/webos/mcp` as a custom connector in Claude (and
|
|
379
|
+
`claude mcp add --transport http webos <url>` for Claude Code):
|
|
380
|
+
```sh
|
|
381
|
+
curl -s http://192.168.1.10:8715/healthz # {"status": "ok"}
|
|
382
|
+
curl -si -X POST https://mcp.example.com/webos/mcp -d '{}' | head -5 # 401 + www-authenticate
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### When it doesn't connect
|
|
386
|
+
|
|
387
|
+
Watch the server while you press Connect: `tail -n 0 -f /var/log/mcp-server-webos/server.log`.
|
|
388
|
+
|
|
389
|
+
| You see | Cause | Fix |
|
|
390
|
+
| --- | --- | --- |
|
|
391
|
+
| The outside `curl` gets a 302 to `…cloudflareaccess.com` | Managed OAuth is off: Claude gets a login page | Turn it on (step 1) |
|
|
392
|
+
| Claude: "couldn't reach" / "authorized, but … returned an error"; Claude Code: `HTTP 502`; nothing in the log | The tunnel can't reach the server: the route's service address isn't where the server listens (usually `localhost` with cloudflared in a container) | Step 2's address and `MCP_HTTP_BIND` must match |
|
|
393
|
+
| Claude Code: `HTTP 421`; log: `Invalid Host header: mcp.example.com` | `MCP_PUBLIC_HOSTS` isn't set | Step 3 |
|
|
394
|
+
| Log: `Denied … no Cloudflare Access assertion` | The request didn't come through Access: a local test with `curl`, or another Access app/Bypass policy on the path | Ignore it for local tests; otherwise see step 1's last point |
|
|
395
|
+
| Log: `Denied … Audience doesn't match` / `Invalid issuer` | `CF_ACCESS_AUD` is another app's tag, or the team domain is wrong | Copy both again from the app in step 1 |
|
|
396
|
+
| Log: `Denied … not in MCP_ALLOWED_EMAILS` | You signed in to Access with another address | Make them match |
|
|
397
|
+
|
|
398
|
+
### As a service on Alpine (Proxmox LXC)
|
|
399
|
+
|
|
400
|
+
```sh
|
|
401
|
+
sh deploy/alpine/install.sh # as root; or: install.sh /path/to/checkout
|
|
402
|
+
su -s /bin/sh mcp-webos -c 'WEBOS_CONFIG_DIR=/var/lib/mcp-server-webos \
|
|
403
|
+
/opt/mcp-server-webos/venv/bin/mcp-server-webos pair --host <tv ip> --name "Family Room"'
|
|
404
|
+
vi /etc/mcp-server-webos/env # HTTP and Access settings
|
|
405
|
+
rc-update add mcp-server-webos default && rc-service mcp-server-webos start
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
| Path | Owner, mode | Holds |
|
|
409
|
+
| --- | --- | --- |
|
|
410
|
+
| `/opt/mcp-server-webos/venv` | root, 0755 | The code |
|
|
411
|
+
| `/etc/mcp-server-webos/env` | root:mcp-webos, 0640 | HTTP and Access settings |
|
|
412
|
+
| `/var/lib/mcp-server-webos/` | mcp-webos, 0700 (files 0600) | `keys.json`, `config.json` |
|
|
413
|
+
| `/var/log/mcp-server-webos/server.log` | mcp-webos, 0750 | Logs, redacted |
|
|
414
|
+
| `/etc/init.d/mcp-server-webos` | root, 0755 | OpenRC script (`supervise-daemon`) |
|
|
415
|
+
|
|
416
|
+
**Wake-on-LAN from an LXC:** the magic packet is a broadcast (plus a copy to the TV's last
|
|
417
|
+
address), so the container must be bridged onto the TVs' network, not NATed. `doctor` shows the
|
|
418
|
+
local address it would leave from.
|
|
419
|
+
|
|
420
|
+
## Logs
|
|
421
|
+
|
|
422
|
+
stderr only (stdout is the MCP stdio transport). Addresses, MACs, serial numbers, deviceUUIDs and
|
|
423
|
+
pointer-socket ids are redacted unless `--no-redact` (or `WEBOS_LOG_UNREDACTED=1`); client keys,
|
|
424
|
+
JWTs and certificates always are. `--debug` (`WEBOS_DEBUG=1`) adds aiowebostv's protocol traffic.
|
|
425
|
+
|
|
426
|
+
## Testing without a TV
|
|
427
|
+
|
|
428
|
+
```sh
|
|
429
|
+
pip install -e ".[dev]" && pytest && ruff check . && ruff format --check . && mypy
|
|
430
|
+
mcp-server-webos simulate --profile g1 --config-dir /tmp/sim --paired # then, in another terminal:
|
|
431
|
+
WEBOS_CONFIG_DIR=/tmp/sim mcp-server-webos call webos_get_status
|
|
432
|
+
WEBOS_CONFIG_DIR=/tmp/sim mcp-server-webos call webos_select_source source=netflix
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
The simulator (`src/webos_mcp/sim/`) speaks SSAP over real WebSockets: the hello, registration
|
|
436
|
+
and the pairing dialog, requests and subscriptions with pushed updates, the TV's error replies,
|
|
437
|
+
the pointer socket, and Wake-on-LAN. The real aiowebostv connects to it. Profiles: `c7`, `g1`,
|
|
438
|
+
`c5-webos25`, `c5-webos26`, each field naming its source. aiowebostv always dials ports 3000 and
|
|
439
|
+
3001, so each simulated TV binds the real ports on its own loopback address (127.0.0.2-5): on
|
|
440
|
+
Linux all of 127.0.0.0/8 is loopback. (macOS needs loopback aliases for that.)
|
|
441
|
+
|
|
442
|
+
**WSL2 with mirrored networking** (`networkingMode=mirrored` in `.wslconfig`): Windows then
|
|
443
|
+
handles loopback and answers only 127.0.0.1, so the simulator tests hang and time out. Run them
|
|
444
|
+
with `scripts/test-wsl.sh [pytest args]`, which runs pytest in a network namespace of its own
|
|
445
|
+
(ordinary Linux loopback, no other network; needs `unshare` and `ip`, and root or sudo). WSL2's
|
|
446
|
+
default NAT networking doesn't need it.
|
|
447
|
+
|
|
448
|
+
`mcp-server-webos capture --tv <name> --profile <p>` records a real TV's answers to read-only
|
|
449
|
+
requests, redacted; committed to `src/webos_mcp/sim/fixtures/`, each capture becomes a contract
|
|
450
|
+
test against its simulator profile.
|
|
451
|
+
|
|
452
|
+
## What has been confirmed on hardware
|
|
453
|
+
|
|
454
|
+
Every protocol detail this server relies on is in `src/webos_mcp/assumptions.py`, with its source
|
|
455
|
+
and how confident that source is. `doctor` prints the unconfirmed ones; HARDWARE_VALIDATION.md
|
|
456
|
+
says which step confirms each.
|
|
457
|
+
|
|
458
|
+
| Assumption | Confidence | Status |
|
|
459
|
+
| --- | --- | --- |
|
|
460
|
+
| `W-MANIFEST` | high | simulator-only |
|
|
461
|
+
| `W-SIGNED-403` | high | simulator-only |
|
|
462
|
+
| `W-KEY-SURVIVES` | low | simulator-only |
|
|
463
|
+
| `W-REGISTERED-DIRECT` | high | hardware-verified |
|
|
464
|
+
| `W-STALE-KEY` | medium | simulator-only |
|
|
465
|
+
| `W-PROMPT-DISMISS` | low | simulator-only |
|
|
466
|
+
| `W-PAIR-WINDOW` | low | simulator-only |
|
|
467
|
+
| `W-HELLO-VERSION` | high | hardware-verified |
|
|
468
|
+
| `W-SWINFO-401` | high | hardware-verified |
|
|
469
|
+
| `W-UUID-STABLE` | medium | simulator-only |
|
|
470
|
+
| `W-PORTS` | medium | simulator-only |
|
|
471
|
+
| `W-PORT3000-CLOSE` | medium | simulator-only |
|
|
472
|
+
| `W-TLS-CERT` | low | simulator-only |
|
|
473
|
+
| `W-MULTI-CLIENT` | medium | simulator-only |
|
|
474
|
+
| `W-INPUT-SOCKET` | medium | hardware-verified |
|
|
475
|
+
| `W-SUBSCRIBE` | high | hardware-verified |
|
|
476
|
+
| `W-HELLO-1008` | medium | hardware-verified |
|
|
477
|
+
| `W-POWER-STATES` | high | simulator-only |
|
|
478
|
+
| `W-C7-NO-POWERSTATE` | low | hardware-contradicted |
|
|
479
|
+
| `W-STANDBY-SOCKET` | low | simulator-only |
|
|
480
|
+
| `W-OFF-GUARD` | low | simulator-only |
|
|
481
|
+
| `W-TURNON-DEAD` | medium | simulator-only |
|
|
482
|
+
| `W-WOL` | medium | simulator-only |
|
|
483
|
+
| `W-WOL-PATH` | low | simulator-only |
|
|
484
|
+
| `W-WAKE-TIME` | medium | hardware-contradicted |
|
|
485
|
+
| `W-MAC-SOURCE` | low | simulator-only |
|
|
486
|
+
| `W-SCREEN-OFF` | medium | simulator-only |
|
|
487
|
+
| `W-KEYS` | medium | simulator-only |
|
|
488
|
+
| `W-MEDIA-CONTROLS` | low | simulator-only |
|
|
489
|
+
| `W-NOW-PLAYING` | medium | hardware-verified |
|
|
490
|
+
| `W-SOUND-OUTPUT` | medium | hardware-verified |
|
|
491
|
+
| `W-VOLUME-ARC` | medium | simulator-only |
|
|
492
|
+
| `W-APP-IDS` | low | simulator-only |
|
|
493
|
+
| `W-SYSTEM-APP-IDS` | medium | simulator-only |
|
|
494
|
+
| `W-INPUT-IDS` | medium | hardware-verified |
|
|
495
|
+
| `W-SSDP` | medium | hardware-verified |
|
|
496
|
+
| `W-IME-FOCUS` | high | hardware-verified |
|
|
497
|
+
| `W-IME-TEXT` | high | hardware-verified |
|
|
498
|
+
| `W-IME-LENGTH` | medium | hardware-verified |
|
|
499
|
+
| `W-YOUTUBE-SEARCH` | medium | hardware-verified |
|
|
500
|
+
|
|
501
|
+
## License
|
|
502
|
+
|
|
503
|
+
MIT
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
mcp_server_webos-0.1.0.dist-info/licenses/LICENSE,sha256=UaFe7iKAtdvNfrhH4lJ-v0InFSbjZGMZ4dLXeVf7o8g,1076
|
|
2
|
+
webos_mcp/__init__.py,sha256=WhsC083wjo_xoTErlAgzB4hZ-NaVJtITLDcOBeOCD2k,251
|
|
3
|
+
webos_mcp/apps.py,sha256=VUruz9bmZGHNJ1GHsENuBLnjLuzbUr00crled3rUxSc,8742
|
|
4
|
+
webos_mcp/assumptions.py,sha256=MXFI9C-_zhFE6pMHSO4i75Pb7F9sH2aiqV4r0Zm59ow,24904
|
|
5
|
+
webos_mcp/capture.py,sha256=3ab_8OW7pVDmbhfRNY4YDf8xBxJ0W_qAiNTXM9RD8MQ,2804
|
|
6
|
+
webos_mcp/cli.py,sha256=0_DLy_IelYz72H6k9ATKaK29MhH9-BS4A-EvXhAuy80,20990
|
|
7
|
+
webos_mcp/client.py,sha256=lwsGMPYQuQDU9mY6ph32cC8yLmhTsDUyrLYbOGhXSiY,52063
|
|
8
|
+
webos_mcp/compat.py,sha256=LXfzwQDuaNCZxWA3qWEGMTjwHZ5nvh4Uicbm_TZdFV8,8407
|
|
9
|
+
webos_mcp/config.py,sha256=ugMxvT0drTJvgO_20p-vL6si7aCREC8G7hGoOTN0LYk,15802
|
|
10
|
+
webos_mcp/discovery.py,sha256=r88MY6W-MooWmH7qad0dqpxeY-P704qQ2tiQDPA_hWc,2445
|
|
11
|
+
webos_mcp/doctor.py,sha256=6RPZmk7oMePLwq4JIXgMiMVo5RV0d-qGFULHTOOZKZM,12888
|
|
12
|
+
webos_mcp/errors.py,sha256=nWFyUJsXtN2W2HEWvLHqzaMUFpLbBmgo-af5yOCt1rk,581
|
|
13
|
+
webos_mcp/keystore.py,sha256=u-bOuLD3EZm9VcXMGJ0sUsrAPGre-q4Ra4uOMIsdTWE,1754
|
|
14
|
+
webos_mcp/logsafe.py,sha256=QuCitdXqckrLQSzb-V0R2pxFhLyqMhPSKQq7s3R97mI,15892
|
|
15
|
+
webos_mcp/lookup.py,sha256=TmP8Abg5drIKTzHZ928YJ8Sa1qSDmvymEhwnSQpun6s,2235
|
|
16
|
+
webos_mcp/probe.py,sha256=fIbWu2dEzWjRxYLILaza2OuH1J8TQHNqmLEtSkUU5qI,2400
|
|
17
|
+
webos_mcp/ratelimit.py,sha256=aIhy8CAPMxJdEdlMvFCjUT8jElFKvb0QcrR8zxAB6Ps,9840
|
|
18
|
+
webos_mcp/redact.py,sha256=Xq-Jz-aSnSqkfqKRBZMx2UlSgB6ra72vYXgBgIcNRHM,2786
|
|
19
|
+
webos_mcp/registry.py,sha256=9JIwD7hYao7m6PWJXZDAiGQfd8dfGWIfruuAJYNFsUw,7027
|
|
20
|
+
webos_mcp/remote.py,sha256=v7d4C9Phz11hc9XsORMJ7VY-GO2GMrdfaa_gEU8b7s8,23958
|
|
21
|
+
webos_mcp/server.py,sha256=lCNIFrR7uOBpzKQbbfGmZkkLZkJ-bdAgPNkB-B4taPI,23030
|
|
22
|
+
webos_mcp/tasks.py,sha256=FttB8XI_evXjIJUyWz7rtjENQVhgTS6ZcbEB5v-crkc,7815
|
|
23
|
+
webos_mcp/tools.py,sha256=TlMObSXFMZst3ssGBrSwtS3WpjbAV0ngKfU5mnajWNU,1363
|
|
24
|
+
webos_mcp/tvs.py,sha256=9h1C_zhlBl_7sDGjwsp44QeJYdSDU8n-ALooIfn60QM,2332
|
|
25
|
+
webos_mcp/wol.py,sha256=E5XtLNqX304shz4qv4Sy-jxt-eZF5OjYrrqmfg9Rh9c,2985
|
|
26
|
+
webos_mcp/sim/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
27
|
+
webos_mcp/sim/fake_tv.py,sha256=EmXKXLgHhrBXt9pvgOrNNhIDu7hAQx_CAcoh2_LJmSs,28037
|
|
28
|
+
webos_mcp/sim/profiles.py,sha256=mVQgwqUIvrsYLyV2-Klts04G0Yc2D0YpyXFw5ikDXJQ,9091
|
|
29
|
+
webos_mcp/sim/fixtures/c5-webos25.json,sha256=LXqwp0b1dlKuEDhIewe3RR_HVJPSM2gkU3FpMx8ySoQ,43637
|
|
30
|
+
webos_mcp/sim/fixtures/c7.json,sha256=fvzxcxfVdrvQpId420-CwrJeJeGmqSEIKp9H3xRA2hE,30527
|
|
31
|
+
webos_mcp/sim/fixtures/g1.json,sha256=ltcljdQn4AFMWXEiXKv_1FXzNd2n2rgjrZ2tvARv410,51201
|
|
32
|
+
mcp_server_webos-0.1.0.dist-info/METADATA,sha256=KUw2q3nmo-gdd9zUrFLOWCw7JArrHX54lXgR0yftRa0,27422
|
|
33
|
+
mcp_server_webos-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
34
|
+
mcp_server_webos-0.1.0.dist-info/entry_points.txt,sha256=w2MHzfTM19dxNQ7OkmC_OyQV4kuRYjnr3i7UKpJ3dJo,56
|
|
35
|
+
mcp_server_webos-0.1.0.dist-info/top_level.txt,sha256=fKUn2MfRHVj4SEjcU5bjgAAJP389OfwkoO3e5zszT8g,10
|
|
36
|
+
mcp_server_webos-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nicholas E. Andrade
|
|
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 @@
|
|
|
1
|
+
webos_mcp
|
webos_mcp/__init__.py
ADDED