aiui-mcp 0.2.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.
- aiui_mcp-0.2.0/.gitignore +29 -0
- aiui_mcp-0.2.0/PKG-INFO +62 -0
- aiui_mcp-0.2.0/README.md +38 -0
- aiui_mcp-0.2.0/pyproject.toml +51 -0
- aiui_mcp-0.2.0/src/aiui_mcp/__init__.py +4 -0
- aiui_mcp-0.2.0/src/aiui_mcp/__main__.py +4 -0
- aiui_mcp-0.2.0/src/aiui_mcp/server.py +341 -0
- aiui_mcp-0.2.0/src/aiui_mcp/skill.md +114 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# build artifacts
|
|
2
|
+
companion/dist/
|
|
3
|
+
companion/src-tauri/target/
|
|
4
|
+
companion/src-tauri/gen/schemas/
|
|
5
|
+
|
|
6
|
+
# deps
|
|
7
|
+
companion/node_modules/
|
|
8
|
+
|
|
9
|
+
# packaged release
|
|
10
|
+
aiui-*.zip
|
|
11
|
+
aiui-*.dmg
|
|
12
|
+
aiui-*.tar.gz
|
|
13
|
+
latest.json
|
|
14
|
+
|
|
15
|
+
# macOS
|
|
16
|
+
.DS_Store
|
|
17
|
+
|
|
18
|
+
# logs / local state
|
|
19
|
+
*.log
|
|
20
|
+
/tmp/aiui-trace.log
|
|
21
|
+
|
|
22
|
+
# IDE / editor
|
|
23
|
+
.vscode/
|
|
24
|
+
.idea/
|
|
25
|
+
*.swp
|
|
26
|
+
.aider*
|
|
27
|
+
|
|
28
|
+
# local release credentials
|
|
29
|
+
.env.release
|
aiui_mcp-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aiui-mcp
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: MCP server for aiui — native macOS dialogs from any Claude Code session, local or remote.
|
|
5
|
+
Project-URL: Homepage, https://github.com/byte5ai/aiui
|
|
6
|
+
Project-URL: Repository, https://github.com/byte5ai/aiui
|
|
7
|
+
Project-URL: Issues, https://github.com/byte5ai/aiui/issues
|
|
8
|
+
Author-email: byte5 GmbH <cw@byte5.de>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Keywords: claude,dialog,macos,mcp,tauri,ui
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: MacOS :: MacOS X
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Software Development :: User Interfaces
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: httpx>=0.27
|
|
22
|
+
Requires-Dist: mcp>=1.26.0
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# aiui-mcp
|
|
26
|
+
|
|
27
|
+
MCP server for [**aiui**](https://github.com/byte5ai/aiui) — lets Claude Code
|
|
28
|
+
sessions render native macOS dialogs on the user's Mac. Works for local and
|
|
29
|
+
remote Claude Code setups.
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
Drop this into your project's `.mcp.json`:
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"mcpServers": {
|
|
38
|
+
"aiui": {
|
|
39
|
+
"command": "uvx",
|
|
40
|
+
"args": ["aiui-mcp"]
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`uvx` pulls the latest version automatically. The companion macOS app is
|
|
47
|
+
distributed separately from <https://github.com/byte5ai/aiui/releases>.
|
|
48
|
+
|
|
49
|
+
## Tools
|
|
50
|
+
|
|
51
|
+
- `aiui.confirm` — hard yes/no with optional destructive styling
|
|
52
|
+
- `aiui.ask` — single- or multi-choice with per-option descriptions
|
|
53
|
+
- `aiui.form` — composite window with typed fields and action buttons
|
|
54
|
+
- `aiui.aiui_health` — reachability check
|
|
55
|
+
|
|
56
|
+
## Prompts
|
|
57
|
+
|
|
58
|
+
- `/aiui:widgets` — full widget catalog with rules, patterns, anti-patterns
|
|
59
|
+
|
|
60
|
+
## License
|
|
61
|
+
|
|
62
|
+
MIT — see [LICENSE](https://github.com/byte5ai/aiui/blob/main/LICENSE).
|
aiui_mcp-0.2.0/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# aiui-mcp
|
|
2
|
+
|
|
3
|
+
MCP server for [**aiui**](https://github.com/byte5ai/aiui) — lets Claude Code
|
|
4
|
+
sessions render native macOS dialogs on the user's Mac. Works for local and
|
|
5
|
+
remote Claude Code setups.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
Drop this into your project's `.mcp.json`:
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"mcpServers": {
|
|
14
|
+
"aiui": {
|
|
15
|
+
"command": "uvx",
|
|
16
|
+
"args": ["aiui-mcp"]
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`uvx` pulls the latest version automatically. The companion macOS app is
|
|
23
|
+
distributed separately from <https://github.com/byte5ai/aiui/releases>.
|
|
24
|
+
|
|
25
|
+
## Tools
|
|
26
|
+
|
|
27
|
+
- `aiui.confirm` — hard yes/no with optional destructive styling
|
|
28
|
+
- `aiui.ask` — single- or multi-choice with per-option descriptions
|
|
29
|
+
- `aiui.form` — composite window with typed fields and action buttons
|
|
30
|
+
- `aiui.aiui_health` — reachability check
|
|
31
|
+
|
|
32
|
+
## Prompts
|
|
33
|
+
|
|
34
|
+
- `/aiui:widgets` — full widget catalog with rules, patterns, anti-patterns
|
|
35
|
+
|
|
36
|
+
## License
|
|
37
|
+
|
|
38
|
+
MIT — see [LICENSE](https://github.com/byte5ai/aiui/blob/main/LICENSE).
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "aiui-mcp"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
description = "MCP server for aiui — native macOS dialogs from any Claude Code session, local or remote."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "byte5 GmbH", email = "cw@byte5.de" },
|
|
10
|
+
]
|
|
11
|
+
keywords = ["mcp", "claude", "ui", "dialog", "macos", "tauri"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 4 - Beta",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Operating System :: MacOS :: MacOS X",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.10",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Topic :: Software Development :: User Interfaces",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"mcp>=1.26.0",
|
|
25
|
+
"httpx>=0.27",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://github.com/byte5ai/aiui"
|
|
30
|
+
Repository = "https://github.com/byte5ai/aiui"
|
|
31
|
+
Issues = "https://github.com/byte5ai/aiui/issues"
|
|
32
|
+
|
|
33
|
+
[project.scripts]
|
|
34
|
+
aiui-mcp = "aiui_mcp.__main__:main"
|
|
35
|
+
|
|
36
|
+
[build-system]
|
|
37
|
+
requires = ["hatchling"]
|
|
38
|
+
build-backend = "hatchling.build"
|
|
39
|
+
|
|
40
|
+
[tool.hatch.build.targets.wheel]
|
|
41
|
+
packages = ["src/aiui_mcp"]
|
|
42
|
+
|
|
43
|
+
[tool.hatch.build.targets.wheel.force-include]
|
|
44
|
+
"src/aiui_mcp/skill.md" = "aiui_mcp/skill.md"
|
|
45
|
+
|
|
46
|
+
[tool.hatch.build.targets.sdist]
|
|
47
|
+
include = [
|
|
48
|
+
"src/aiui_mcp",
|
|
49
|
+
"README.md",
|
|
50
|
+
"pyproject.toml",
|
|
51
|
+
]
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
"""aiui MCP server — renders native macOS dialogs via the aiui companion.
|
|
2
|
+
|
|
3
|
+
Topology:
|
|
4
|
+
|
|
5
|
+
Claude Code (local or remote) ──stdio──► aiui-mcp (this process)
|
|
6
|
+
│ HTTP
|
|
7
|
+
▼
|
|
8
|
+
http://127.0.0.1:7777
|
|
9
|
+
│ (local, or via SSH reverse-tunnel)
|
|
10
|
+
▼
|
|
11
|
+
Mac: aiui.app (Tauri companion)
|
|
12
|
+
|
|
13
|
+
The aiui token is read from `~/.config/aiui/token` — installed once when
|
|
14
|
+
the companion runs on the Mac, and scp'd automatically to each remote host
|
|
15
|
+
registered in the companion's settings window.
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import importlib.metadata
|
|
20
|
+
import importlib.resources as resources
|
|
21
|
+
import logging
|
|
22
|
+
import os
|
|
23
|
+
import sys
|
|
24
|
+
from datetime import datetime, timezone
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
import httpx
|
|
29
|
+
from mcp.server.fastmcp import FastMCP
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _version() -> str:
|
|
33
|
+
try:
|
|
34
|
+
return importlib.metadata.version("aiui-mcp")
|
|
35
|
+
except importlib.metadata.PackageNotFoundError:
|
|
36
|
+
return "dev"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
VERSION = _version()
|
|
40
|
+
BUILD_INFO = f"aiui-mcp v{VERSION}"
|
|
41
|
+
|
|
42
|
+
logging.basicConfig(
|
|
43
|
+
level=os.environ.get("AIUI_LOG_LEVEL", "INFO").upper(),
|
|
44
|
+
format="%(asctime)s.%(msecs)03d %(levelname)s %(name)s %(message)s",
|
|
45
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
46
|
+
stream=sys.stderr,
|
|
47
|
+
)
|
|
48
|
+
log = logging.getLogger("aiui")
|
|
49
|
+
log.info("---- %s started pid=%d ----", BUILD_INFO, os.getpid())
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
TOKEN_PATH = Path(os.environ.get("AIUI_TOKEN_PATH", "~/.config/aiui/token")).expanduser()
|
|
53
|
+
ENDPOINT = os.environ.get("AIUI_ENDPOINT", "http://127.0.0.1:7777")
|
|
54
|
+
TIMEOUT_S = float(os.environ.get("AIUI_TIMEOUT_S", "120"))
|
|
55
|
+
HEALTH_TIMEOUT_S = float(os.environ.get("AIUI_HEALTH_TIMEOUT_S", "3"))
|
|
56
|
+
|
|
57
|
+
mcp = FastMCP("aiui")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _token() -> str:
|
|
61
|
+
if not TOKEN_PATH.exists():
|
|
62
|
+
raise RuntimeError(
|
|
63
|
+
f"aiui token not found at {TOKEN_PATH}. "
|
|
64
|
+
"Install the aiui companion on your Mac and register this remote from its "
|
|
65
|
+
"settings window (adds the token automatically). "
|
|
66
|
+
"Download: https://github.com/byte5ai/aiui/releases/latest"
|
|
67
|
+
)
|
|
68
|
+
return TOKEN_PATH.read_text().strip()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
async def _preflight() -> None:
|
|
72
|
+
"""Quick sanity check before every render call: the service on :7777 must
|
|
73
|
+
accept our bearer token. Guards against stale local aiui instances that
|
|
74
|
+
would otherwise hijack the SSH reverse-forward and hang dialogs silently.
|
|
75
|
+
"""
|
|
76
|
+
async with httpx.AsyncClient(timeout=HEALTH_TIMEOUT_S) as client:
|
|
77
|
+
try:
|
|
78
|
+
r = await client.get(
|
|
79
|
+
f"{ENDPOINT}/health",
|
|
80
|
+
headers={"Authorization": f"Bearer {_token()}"},
|
|
81
|
+
)
|
|
82
|
+
except httpx.ConnectError as e:
|
|
83
|
+
raise RuntimeError(
|
|
84
|
+
f"aiui companion not reachable at {ENDPOINT}. "
|
|
85
|
+
f"Is Claude Desktop running on your Mac? For remote projects, the "
|
|
86
|
+
f"SSH reverse-tunnel must also be active (companion handles it "
|
|
87
|
+
f"automatically if this host is registered in its settings). "
|
|
88
|
+
f"Underlying error: {e}"
|
|
89
|
+
) from e
|
|
90
|
+
except httpx.ReadTimeout as e:
|
|
91
|
+
raise RuntimeError(
|
|
92
|
+
f"aiui companion at {ENDPOINT} timed out on /health — likely a stale "
|
|
93
|
+
f"local aiui instance holding the port. Run `pkill -f '^aiui$'` on "
|
|
94
|
+
f"this host. ({e})"
|
|
95
|
+
) from e
|
|
96
|
+
|
|
97
|
+
if r.status_code == 401:
|
|
98
|
+
raise RuntimeError(
|
|
99
|
+
f"aiui companion at {ENDPOINT} rejected our token (401). "
|
|
100
|
+
f"Another aiui process may be listening on this port with a different "
|
|
101
|
+
f"token. Run `pkill -f '^aiui$'` on this host, then re-register it "
|
|
102
|
+
f"from the companion's settings window to re-sync the token."
|
|
103
|
+
)
|
|
104
|
+
if r.status_code != 200:
|
|
105
|
+
raise RuntimeError(
|
|
106
|
+
f"aiui companion /health returned {r.status_code}: {r.text[:200]}"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
async def _post_render(spec: dict[str, Any]) -> dict[str, Any]:
|
|
111
|
+
await _preflight()
|
|
112
|
+
t0 = datetime.now(timezone.utc)
|
|
113
|
+
log.info("render → kind=%s", spec.get("kind"))
|
|
114
|
+
async with httpx.AsyncClient(timeout=TIMEOUT_S) as client:
|
|
115
|
+
r = await client.post(
|
|
116
|
+
f"{ENDPOINT}/render",
|
|
117
|
+
headers={"Authorization": f"Bearer {_token()}"},
|
|
118
|
+
json={"spec": spec},
|
|
119
|
+
)
|
|
120
|
+
r.raise_for_status()
|
|
121
|
+
dt = (datetime.now(timezone.utc) - t0).total_seconds()
|
|
122
|
+
data = r.json()
|
|
123
|
+
log.info(
|
|
124
|
+
"render ← kind=%s cancelled=%s took=%.2fs",
|
|
125
|
+
spec.get("kind"), data.get("cancelled"), dt,
|
|
126
|
+
)
|
|
127
|
+
return data
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _format_result(payload: dict[str, Any]) -> dict[str, Any]:
|
|
131
|
+
if payload.get("cancelled"):
|
|
132
|
+
return {"cancelled": True}
|
|
133
|
+
return {"cancelled": False, **payload.get("result", {})}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@mcp.tool()
|
|
137
|
+
async def ask(
|
|
138
|
+
question: str,
|
|
139
|
+
options: list[dict[str, str]],
|
|
140
|
+
header: str | None = None,
|
|
141
|
+
multi_select: bool = False,
|
|
142
|
+
allow_other: bool = True,
|
|
143
|
+
) -> dict[str, Any]:
|
|
144
|
+
"""Single- or multi-choice picker with optional free-text fallback.
|
|
145
|
+
|
|
146
|
+
WHEN TO USE: 2–6 mutually-exclusive options where per-option context helps.
|
|
147
|
+
For yes/no, use `confirm`. For mixed inputs, use `form`.
|
|
148
|
+
|
|
149
|
+
WRITE OPTIONS:
|
|
150
|
+
- Label: noun or short imperative, ≤ 5 words, no punctuation, no emoji.
|
|
151
|
+
- Description: one sentence stating the trade-off or consequence.
|
|
152
|
+
- Keep options parallel in grammar.
|
|
153
|
+
|
|
154
|
+
ANTI-PATTERNS: > 8 options (use `form` with a `list` field); generic labels
|
|
155
|
+
like "Option 1"; redundant descriptions that just restate the label.
|
|
156
|
+
|
|
157
|
+
Returns `{cancelled, answers, other?}`. `answers` is a list of values.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
question: Full question, imperative or interrogative.
|
|
161
|
+
options: List of `{"label": str, "description"?: str, "value"?: str}`.
|
|
162
|
+
header: Short chip above the question (≤ 14 chars).
|
|
163
|
+
multi_select: Allow selecting multiple options.
|
|
164
|
+
allow_other: Offer a free-text fallback.
|
|
165
|
+
"""
|
|
166
|
+
spec = {
|
|
167
|
+
"kind": "ask",
|
|
168
|
+
"question": question,
|
|
169
|
+
"header": header,
|
|
170
|
+
"options": options,
|
|
171
|
+
"multiSelect": multi_select,
|
|
172
|
+
"allowOther": allow_other,
|
|
173
|
+
}
|
|
174
|
+
return _format_result(await _post_render(spec))
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@mcp.tool()
|
|
178
|
+
async def form(
|
|
179
|
+
title: str,
|
|
180
|
+
fields: list[dict[str, Any]],
|
|
181
|
+
description: str | None = None,
|
|
182
|
+
header: str | None = None,
|
|
183
|
+
actions: list[dict[str, Any]] | None = None,
|
|
184
|
+
submit_label: str | None = None,
|
|
185
|
+
cancel_label: str | None = None,
|
|
186
|
+
) -> dict[str, Any]:
|
|
187
|
+
"""Composite window: multiple typed fields + multiple action buttons.
|
|
188
|
+
|
|
189
|
+
WHEN TO USE: ≥ 2 related inputs, or one input plus context/confirmation.
|
|
190
|
+
For yes/no, use `confirm`. For a single choice, use `ask`.
|
|
191
|
+
|
|
192
|
+
WRITE LABELS:
|
|
193
|
+
- Imperative or noun, ≤ 6 words, no punctuation, no emoji.
|
|
194
|
+
- Consistent register across all fields.
|
|
195
|
+
- Field-level descriptions only if the label alone is ambiguous.
|
|
196
|
+
|
|
197
|
+
BE RESTRAINT:
|
|
198
|
+
- ≤ 8 fields per dialog. Split logically if you need more.
|
|
199
|
+
- `static_text` only for context the user couldn't derive from labels.
|
|
200
|
+
- Defaults that a human would actually pick.
|
|
201
|
+
|
|
202
|
+
ACTION BUTTONS:
|
|
203
|
+
- Verb-based and concrete ("Create report"), not "OK".
|
|
204
|
+
- Destructive actions: `destructive: true` — never red a save button.
|
|
205
|
+
- `skip_validation: true` on escape hatches so required-field validation
|
|
206
|
+
doesn't trap the user.
|
|
207
|
+
- ≤ 3 actions.
|
|
208
|
+
|
|
209
|
+
FIELD KINDS:
|
|
210
|
+
- text: {kind, name, label, placeholder?, default?, multiline?, required?}
|
|
211
|
+
- password: {kind, name, label, placeholder?, required?} — masked, treat as secret
|
|
212
|
+
- number: {kind, name, label, default?, min?, max?, step?, required?}
|
|
213
|
+
- select: {kind, name, label, options: [{label, value}], default?, required?}
|
|
214
|
+
- checkbox: {kind, name, label, default?}
|
|
215
|
+
- slider: {kind, name, label, min, max, step?, default?}
|
|
216
|
+
- date: {kind, name, label, default?, required?} — ISO YYYY-MM-DD
|
|
217
|
+
- date_range: {kind, name, label, default?: {from, to}, required?} — result {from, to}
|
|
218
|
+
- color: {kind, name, label, default?} — hex "#RRGGBB"
|
|
219
|
+
- static_text: {kind, text, tone?: "info"|"warn"|"muted"} — display only
|
|
220
|
+
- list: {kind, name, label?, items: [{label, value, description?}],
|
|
221
|
+
selectable?, multi_select?, sortable?, default_selected?: [values]}
|
|
222
|
+
Result: {selected: [values], order: [values]}
|
|
223
|
+
- tree: {kind, name, label?, items: [{label, value, description?, children?: [...]}],
|
|
224
|
+
multi_select?, default_selected?: [values], default_expanded?: [values]}
|
|
225
|
+
Result: {selected: [values]}
|
|
226
|
+
|
|
227
|
+
Returns `{cancelled, action?, values: {name: value, ...}}`.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
title: Window title. Same rules as labels.
|
|
231
|
+
fields: List of field blocks, each with a `kind` from above.
|
|
232
|
+
description: Subtitle, ≤ 2 sentences.
|
|
233
|
+
header: Chip above the title (≤ 14 chars).
|
|
234
|
+
actions: Footer buttons `[{label, value, primary?, destructive?, skip_validation?}]`.
|
|
235
|
+
Without actions, defaults to Cancel + Submit.
|
|
236
|
+
submit_label: Legacy fallback for the default submit button label.
|
|
237
|
+
cancel_label: Legacy fallback for the default cancel button label.
|
|
238
|
+
"""
|
|
239
|
+
spec = {
|
|
240
|
+
"kind": "form",
|
|
241
|
+
"title": title,
|
|
242
|
+
"description": description,
|
|
243
|
+
"header": header,
|
|
244
|
+
"fields": fields,
|
|
245
|
+
"actions": actions,
|
|
246
|
+
"submitLabel": submit_label,
|
|
247
|
+
"cancelLabel": cancel_label,
|
|
248
|
+
}
|
|
249
|
+
return _format_result(await _post_render(spec))
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
@mcp.tool()
|
|
253
|
+
async def confirm(
|
|
254
|
+
title: str,
|
|
255
|
+
message: str | None = None,
|
|
256
|
+
header: str | None = None,
|
|
257
|
+
destructive: bool = False,
|
|
258
|
+
confirm_label: str | None = None,
|
|
259
|
+
cancel_label: str | None = None,
|
|
260
|
+
) -> dict[str, Any]:
|
|
261
|
+
"""Hard yes/no decision with optional destructive styling.
|
|
262
|
+
|
|
263
|
+
WHEN TO USE: irreversible or high-stakes step where "just proceed" is
|
|
264
|
+
unsafe. For pure information, respond in chat. For 3+ options, use `ask`.
|
|
265
|
+
|
|
266
|
+
WRITE:
|
|
267
|
+
- Title: the decision as a question, ≤ 10 words.
|
|
268
|
+
- Message: one sentence stating the concrete consequence.
|
|
269
|
+
- `destructive=True` for deletions/force-pushes/rollbacks — never for
|
|
270
|
+
saves or creates.
|
|
271
|
+
- Custom `confirm_label`/`cancel_label` when verbs clarify.
|
|
272
|
+
|
|
273
|
+
Returns `{cancelled, confirmed}`. `cancelled=True` means Escape or window
|
|
274
|
+
close. `cancelled=False, confirmed=False` means the explicit No button.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
title: The decision phrased as a question.
|
|
278
|
+
message: One-sentence explanation of what happens on confirm.
|
|
279
|
+
header: Chip above the title.
|
|
280
|
+
destructive: Red confirm button.
|
|
281
|
+
confirm_label: Defaults to "Ja".
|
|
282
|
+
cancel_label: Defaults to "Nein".
|
|
283
|
+
"""
|
|
284
|
+
spec = {
|
|
285
|
+
"kind": "confirm",
|
|
286
|
+
"title": title,
|
|
287
|
+
"message": message,
|
|
288
|
+
"header": header,
|
|
289
|
+
"destructive": destructive,
|
|
290
|
+
"confirmLabel": confirm_label,
|
|
291
|
+
"cancelLabel": cancel_label,
|
|
292
|
+
}
|
|
293
|
+
return _format_result(await _post_render(spec))
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
@mcp.prompt()
|
|
297
|
+
def widgets() -> str:
|
|
298
|
+
"""The full aiui widget catalog — when to use which dialog, copy
|
|
299
|
+
conventions, anti-patterns, example payloads. Read before composing the
|
|
300
|
+
first dialog in a session."""
|
|
301
|
+
try:
|
|
302
|
+
return (resources.files("aiui_mcp") / "skill.md").read_text()
|
|
303
|
+
except Exception:
|
|
304
|
+
return (
|
|
305
|
+
"aiui skill doc not bundled with this install. "
|
|
306
|
+
"See https://github.com/byte5ai/aiui/blob/main/docs/skill.md"
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
@mcp.tool()
|
|
311
|
+
async def aiui_health() -> dict[str, Any]:
|
|
312
|
+
"""Reachability + token check against the aiui companion.
|
|
313
|
+
|
|
314
|
+
Use this first if dialogs hang or fail — it distinguishes a cold companion
|
|
315
|
+
(user needs to launch Claude Desktop, or the SSH tunnel is down) from a
|
|
316
|
+
rogue local process holding the port with the wrong token.
|
|
317
|
+
"""
|
|
318
|
+
try:
|
|
319
|
+
async with httpx.AsyncClient(timeout=HEALTH_TIMEOUT_S) as client:
|
|
320
|
+
r = await client.get(
|
|
321
|
+
f"{ENDPOINT}/health",
|
|
322
|
+
headers={"Authorization": f"Bearer {_token()}"},
|
|
323
|
+
)
|
|
324
|
+
r.raise_for_status()
|
|
325
|
+
data = r.json()
|
|
326
|
+
return {"ok": True, **data, "endpoint": ENDPOINT, "server": BUILD_INFO}
|
|
327
|
+
except Exception as e:
|
|
328
|
+
log.warning("health check failed: %s", e)
|
|
329
|
+
return {"ok": False, "error": str(e), "endpoint": ENDPOINT, "server": BUILD_INFO}
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def main() -> None:
|
|
333
|
+
"""Entry point for the `aiui-mcp` console script. Default transport is
|
|
334
|
+
stdio (what Claude Code expects). Legacy `--stdio` flag is accepted for
|
|
335
|
+
compatibility with the old script-based invocation."""
|
|
336
|
+
# stdio is the only transport we support; flag-parsing kept minimal.
|
|
337
|
+
mcp.run(transport="stdio")
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
if __name__ == "__main__":
|
|
341
|
+
main()
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: aiui widgets
|
|
3
|
+
description: Render native macOS dialogs on the user's Mac from any Claude Code session — remote or local. Use when the user benefits from structured input (multi-field forms, sortable lists, sliders) more than from chat.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# aiui — Dialog design for Claude agents
|
|
7
|
+
|
|
8
|
+
aiui exposes three MCP tools that render native dialogs on the user's Mac:
|
|
9
|
+
|
|
10
|
+
- `confirm` — irreversible yes/no
|
|
11
|
+
- `ask` — single- or multi-choice with descriptions and optional free-text fallback
|
|
12
|
+
- `form` — composite window with typed fields and multiple action buttons
|
|
13
|
+
|
|
14
|
+
## When to reach for a dialog vs. chat
|
|
15
|
+
|
|
16
|
+
Prefer chat when the answer fits in one line and the user would type it
|
|
17
|
+
anyway. Prefer a dialog when:
|
|
18
|
+
|
|
19
|
+
- Structured input beats free-form typing (numbers in a range, dates,
|
|
20
|
+
multi-select, ordered lists, secrets).
|
|
21
|
+
- You need several related inputs collected in one step.
|
|
22
|
+
- The decision is destructive or high-stakes and benefits from a clearly-framed
|
|
23
|
+
confirmation.
|
|
24
|
+
|
|
25
|
+
Do **not** use a dialog to display information the chat can render just as
|
|
26
|
+
well (status reports, tables, code snippets, logs).
|
|
27
|
+
|
|
28
|
+
## Tool choice
|
|
29
|
+
|
|
30
|
+
| Intent | Tool |
|
|
31
|
+
|---|---|
|
|
32
|
+
| Yes/no, especially destructive | `confirm` |
|
|
33
|
+
| 2–6 options, possibly with per-option context | `ask` |
|
|
34
|
+
| Multi-field input, multi-action footer | `form` |
|
|
35
|
+
| Single free-text answer | just ask in chat |
|
|
36
|
+
| More than 8 fields | split into multiple `form` calls; do not cram one dialog |
|
|
37
|
+
|
|
38
|
+
## Writing labels and copy
|
|
39
|
+
|
|
40
|
+
- Imperative or noun, ≤ 6 words per label, no punctuation, no emoji.
|
|
41
|
+
- Parallel grammar within a dialog. Mix of styles ("Name" / "Bitte geben Sie
|
|
42
|
+
Ihr Alter ein" / "What's your role?") reads as AI slop.
|
|
43
|
+
- Defaults that a real user would pick, not `"enter value here"`.
|
|
44
|
+
- `description`/`static_text` only when the label alone is ambiguous —
|
|
45
|
+
avoid redundancy.
|
|
46
|
+
|
|
47
|
+
## Action buttons (form only)
|
|
48
|
+
|
|
49
|
+
- Verb-based, concrete. `"Bericht erstellen"` beats `"OK"`.
|
|
50
|
+
- Destructive → `destructive: true`. Never style a save button red.
|
|
51
|
+
- Offer an escape hatch (`skip_validation: true`) so required-field validation
|
|
52
|
+
never traps the user.
|
|
53
|
+
- ≤ 3 actions. If you're tempted to add a fourth, rethink the flow.
|
|
54
|
+
|
|
55
|
+
## The `list` field — one widget, four modes
|
|
56
|
+
|
|
57
|
+
| `selectable` | `multi_select` | `sortable` | Mode |
|
|
58
|
+
|---|---|---|---|
|
|
59
|
+
| – | – | – | Static info list |
|
|
60
|
+
| ✓ | – | – | Single-choice (radio) |
|
|
61
|
+
| ✓ | ✓ | – | Multi-choice (checkboxes) |
|
|
62
|
+
| – | – | ✓ | Ordering via drag handles |
|
|
63
|
+
| ✓ | ✓ | ✓ | Pick-and-order |
|
|
64
|
+
|
|
65
|
+
Result is always `{selected: [values], order: [values]}` — `order` reflects
|
|
66
|
+
drag changes, `selected` reflects checkbox state.
|
|
67
|
+
|
|
68
|
+
## Password fields
|
|
69
|
+
|
|
70
|
+
Never collect credentials via `ask` or chat. Use `form` with a `password`
|
|
71
|
+
field so the value stays out of the transcript.
|
|
72
|
+
|
|
73
|
+
## Anti-patterns (gut vs. schlecht)
|
|
74
|
+
|
|
75
|
+
| Slop | Clean |
|
|
76
|
+
|---|---|
|
|
77
|
+
| `confirm(title="Sicher?")` | `confirm(title="Tabelle 'orders' löschen?", destructive=True, message="18.432 Zeilen werden entfernt.")` |
|
|
78
|
+
| `ask(question="Wählen", options=[{"label": "Option 1"}, …])` | `ask(question="Welche Strategie für die Migration?", options=[{"label":"In-place","description":"Schnell, kein Rollback."}, …])` |
|
|
79
|
+
| `form` mit 15 `text`-Feldern | Aufteilen in logische Schritte oder ganz in Chat verlagern |
|
|
80
|
+
| Button-Labels "OK" / "Abbrechen" | "Deploy starten" / "Verwerfen" |
|
|
81
|
+
| `static_text` echot den Titel | `static_text` ergänzt Kontext, den die Labels nicht transportieren |
|
|
82
|
+
|
|
83
|
+
## Quick reference example
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
aiui.form(
|
|
87
|
+
title="Neuer Feature-Entwurf",
|
|
88
|
+
header="Discovery",
|
|
89
|
+
fields=[
|
|
90
|
+
{"kind": "text", "name": "job", "label": "User-Job",
|
|
91
|
+
"multiline": True, "required": True},
|
|
92
|
+
{"kind": "select", "name": "scope", "label": "Umfang",
|
|
93
|
+
"options": [{"label": "Quick Win", "value": "qw"},
|
|
94
|
+
{"label": "Feature", "value": "f"},
|
|
95
|
+
{"label": "Epic", "value": "e"}],
|
|
96
|
+
"default": "f"},
|
|
97
|
+
{"kind": "list", "name": "stakeholders", "label": "Beteiligte",
|
|
98
|
+
"items": [{"label": "Produkt", "value": "prod"},
|
|
99
|
+
{"label": "Design", "value": "design"},
|
|
100
|
+
{"label": "Engineering", "value": "eng"}],
|
|
101
|
+
"selectable": True, "multi_select": True,
|
|
102
|
+
"default_selected": ["prod", "eng"]},
|
|
103
|
+
{"kind": "date", "name": "deadline", "label": "Zieldatum"},
|
|
104
|
+
],
|
|
105
|
+
actions=[
|
|
106
|
+
{"label": "Abbrechen", "value": "cancel", "skip_validation": True},
|
|
107
|
+
{"label": "Entwurf speichern", "value": "draft", "skip_validation": True},
|
|
108
|
+
{"label": "Anlegen", "value": "commit", "primary": True},
|
|
109
|
+
],
|
|
110
|
+
)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Response: `{cancelled: false, action: "commit", values: {job: "...",
|
|
114
|
+
scope: "f", stakeholders: {selected: [...], order: [...]}, deadline: "..."}}`.
|