@paneui/mcp 0.0.19
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.
- package/LICENSE +21 -0
- package/README.md +125 -0
- package/dist/config.js +172 -0
- package/dist/index.js +76 -0
- package/dist/server.js +69 -0
- package/dist/tools.js +340 -0
- package/dist/version.js +3 -0
- package/package.json +55 -0
- package/server.json +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lalit Singh
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# @paneui/mcp
|
|
2
|
+
|
|
3
|
+
A thin **stdio [Model Context Protocol](https://modelcontextprotocol.io) server** for [Pane](https://github.com/aerolalit/paneui). It lets any MCP client — Claude Desktop, Cursor, Windsurf, Cline, your own host — hand a human a rich interactive UI by URL and get structured data back: forms, approvals, pickers, surveys, dashboards, diff/doc review, multi-step wizards.
|
|
4
|
+
|
|
5
|
+
It is a wrapper, not a reimplementation: all relay I/O goes through [`@paneui/core`](https://www.npmjs.com/package/@paneui/core), and config is shared with the [`pane` CLI](https://www.npmjs.com/package/@paneui/cli) (`~/.config/pane/config.json`) — so the CLI and this server use the **same agent identity**.
|
|
6
|
+
|
|
7
|
+
## Runtime requirement: Node.js >= 20
|
|
8
|
+
|
|
9
|
+
The binary is `pane-mcp`. It speaks MCP over stdio and is meant to be launched by an MCP host, not run interactively.
|
|
10
|
+
|
|
11
|
+
## Quickstart
|
|
12
|
+
|
|
13
|
+
No global install needed — point your MCP client at `npx @paneui/mcp`. On first use, if no API key is configured, the server auto-registers a fresh agent against the hosted relay and saves the key to the shared CLI store; nothing else to set up.
|
|
14
|
+
|
|
15
|
+
### Claude Desktop
|
|
16
|
+
|
|
17
|
+
Edit `claude_desktop_config.json` (macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`):
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"mcpServers": {
|
|
22
|
+
"pane": {
|
|
23
|
+
"command": "npx",
|
|
24
|
+
"args": ["-y", "@paneui/mcp"]
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
To pin an existing agent key instead of auto-registering, add an `env` block:
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"mcpServers": {
|
|
35
|
+
"pane": {
|
|
36
|
+
"command": "npx",
|
|
37
|
+
"args": ["-y", "@paneui/mcp"],
|
|
38
|
+
"env": { "PANE_API_KEY": "pane_..." }
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Cursor
|
|
45
|
+
|
|
46
|
+
Add to `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (project):
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"mcpServers": {
|
|
51
|
+
"pane": {
|
|
52
|
+
"command": "npx",
|
|
53
|
+
"args": ["-y", "@paneui/mcp"],
|
|
54
|
+
"env": { "PANE_API_KEY": "pane_..." }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Generic MCP host
|
|
61
|
+
|
|
62
|
+
Any client that takes a `command` + `args` + `env` works the same way:
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{
|
|
66
|
+
"mcpServers": {
|
|
67
|
+
"pane": {
|
|
68
|
+
"command": "npx",
|
|
69
|
+
"args": ["-y", "@paneui/mcp"],
|
|
70
|
+
"env": {
|
|
71
|
+
"PANE_URL": "https://relay.paneui.com",
|
|
72
|
+
"PANE_API_KEY": "pane_..."
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
If you'd rather install it globally (`npm i -g @paneui/mcp`), use `"command": "pane-mcp"` with no `args`.
|
|
80
|
+
|
|
81
|
+
## Configuration
|
|
82
|
+
|
|
83
|
+
All environment variables are optional — the defaults target the hosted relay and auto-register on first use.
|
|
84
|
+
|
|
85
|
+
| Variable | Default | Purpose |
|
|
86
|
+
| --- | --- | --- |
|
|
87
|
+
| `PANE_URL` | `https://relay.paneui.com` | Relay base URL. Set to point at a self-hosted relay. |
|
|
88
|
+
| `PANE_API_KEY` | _(auto-registered)_ | Agent API key. If unset, the server registers an agent on first use and saves the key to `~/.config/pane/config.json` (shared with the CLI). |
|
|
89
|
+
| `PANE_TOKEN` | — | Alias for `PANE_API_KEY` (for hosts that name secrets `*_TOKEN`). `PANE_API_KEY` wins if both are set. |
|
|
90
|
+
| `PANE_AGENT_NAME` | `pane-mcp` | Display name for the auto-registered agent. |
|
|
91
|
+
| `PANE_REGISTER_SECRET` | — | Registration secret, only for relays running `REGISTRATION_MODE=secret`. |
|
|
92
|
+
|
|
93
|
+
Config precedence mirrors the CLI: env vars win over the saved profile, which falls back to the default relay URL.
|
|
94
|
+
|
|
95
|
+
## Tools
|
|
96
|
+
|
|
97
|
+
MCP tools are request/response — there is no long-lived "watch". To receive a human's response you **poll** `get_events` with the cursor from the previous call (optionally with `wait_seconds` to long-poll). Each tool description spells out the pattern for the model.
|
|
98
|
+
|
|
99
|
+
| Tool | What it does |
|
|
100
|
+
| --- | --- |
|
|
101
|
+
| `create_pane` | Create a pane from inline HTML (+ optional event/record schema). Returns `{ pane_id, url, expires_at }`. **Give `url` to the human.** |
|
|
102
|
+
| `get_pane_state` | Fetch a pane's metadata (status, title, expiry) without its event log. |
|
|
103
|
+
| `get_events` | Poll the pane's append-only event log for what the human did. Pass `since` (cursor) and optional `wait_seconds` (long-poll). |
|
|
104
|
+
| `send_to_pane` | Push an event into an open pane to update the live UI. |
|
|
105
|
+
| `list_records` | List rows in a pane's mutable record collection (todos, line items, comments…). |
|
|
106
|
+
| `upsert_record` | Create/return a record row (dedups on `record_key`). |
|
|
107
|
+
| `update_record` | Update a record row (optional `if_match` optimistic lock). |
|
|
108
|
+
| `delete_record` | Soft-delete a record row (the page sees it live). |
|
|
109
|
+
|
|
110
|
+
**Events vs records.** Events are an append-only journal — forms, approvals, surveys, pickers. Records are a mutable collection where the current state matters more than the edit history — todo lists, kanban boards, comment threads. Reach for records when the page shows several mutable items.
|
|
111
|
+
|
|
112
|
+
## Typical flow
|
|
113
|
+
|
|
114
|
+
1. `create_pane` with your HTML + an `event_schema` declaring the events the page emits → returns a `url`.
|
|
115
|
+
2. Paste the `url` into the conversation and ask the human to open it.
|
|
116
|
+
3. `get_events` with `wait_seconds: 25` in a loop, passing the prior `next_cursor` as `since`, until the awaited event appears.
|
|
117
|
+
4. Optionally `send_to_pane` to update the live UI, or use the record tools for mutable collections.
|
|
118
|
+
|
|
119
|
+
## MCP registry
|
|
120
|
+
|
|
121
|
+
`server.json` (in this package) carries the metadata for the [official MCP registry](https://registry.modelcontextprotocol.io). Publishing there is a follow-up step for the maintainer.
|
|
122
|
+
|
|
123
|
+
## License
|
|
124
|
+
|
|
125
|
+
MIT — see [LICENSE](./LICENSE).
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// Relay config resolution for the MCP server.
|
|
2
|
+
//
|
|
3
|
+
// Mirrors how `@paneui/cli` resolves config (see packages/cli/src/config.ts +
|
|
4
|
+
// store.ts) and shares the SAME on-disk store — ${XDG_CONFIG_HOME or
|
|
5
|
+
// ~/.config}/pane/config.json — so a key obtained via `pane agent register`
|
|
6
|
+
// is reused here, and a key obtained here (auto-register on first use) is
|
|
7
|
+
// reused by the CLI.
|
|
8
|
+
//
|
|
9
|
+
// Precedence (highest first):
|
|
10
|
+
// url: PANE_URL env → active profile's url → DEFAULT_RELAY_URL
|
|
11
|
+
// apiKey: PANE_API_KEY → PANE_TOKEN → active profile's api_key
|
|
12
|
+
//
|
|
13
|
+
// PANE_TOKEN is accepted as an alias for PANE_API_KEY: MCP host config files
|
|
14
|
+
// (Claude Desktop / Cursor) commonly name secrets "*_TOKEN", and the task
|
|
15
|
+
// brief calls it PANE_TOKEN. PANE_API_KEY wins if both are set.
|
|
16
|
+
//
|
|
17
|
+
// The store is read/written WITHOUT a dependency on @paneui/cli (it doesn't
|
|
18
|
+
// export its store module). The on-disk shape is kept byte-compatible with
|
|
19
|
+
// the CLI's store.ts so the two stay interchangeable.
|
|
20
|
+
import { readFileSync, writeFileSync, mkdirSync, chmodSync } from "node:fs";
|
|
21
|
+
import { homedir } from "node:os";
|
|
22
|
+
import { join, dirname } from "node:path";
|
|
23
|
+
import { PaneClient, registerAgent } from "@paneui/core";
|
|
24
|
+
/**
|
|
25
|
+
* The hosted Pane relay — the URL fallback when nothing else is set. A
|
|
26
|
+
* self-hoster overrides it with PANE_URL or a registered profile.
|
|
27
|
+
*/
|
|
28
|
+
export const DEFAULT_RELAY_URL = "https://relay.paneui.com";
|
|
29
|
+
/**
|
|
30
|
+
* Profile name used when this server auto-registers a fresh agent. Matches the
|
|
31
|
+
* CLI's DEFAULT_PROFILE_NAME so the two share the same default identity.
|
|
32
|
+
*/
|
|
33
|
+
export const DEFAULT_PROFILE_NAME = "default";
|
|
34
|
+
/** Absolute path to the shared CLI/MCP config file (honours XDG_CONFIG_HOME). */
|
|
35
|
+
export function storePath() {
|
|
36
|
+
const base = process.env.XDG_CONFIG_HOME && process.env.XDG_CONFIG_HOME.trim() !== ""
|
|
37
|
+
? process.env.XDG_CONFIG_HOME
|
|
38
|
+
: join(homedir(), ".config");
|
|
39
|
+
return join(base, "pane", "config.json");
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Read the persisted store. Returns an empty store if the file is missing,
|
|
43
|
+
* unparseable, or malformed — mirrors the CLI's tolerant reader so a corrupt
|
|
44
|
+
* file degrades to "no saved profile" instead of crashing.
|
|
45
|
+
*/
|
|
46
|
+
function readStore() {
|
|
47
|
+
let text;
|
|
48
|
+
try {
|
|
49
|
+
text = readFileSync(storePath(), "utf8");
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return { profiles: {} };
|
|
53
|
+
}
|
|
54
|
+
let parsed;
|
|
55
|
+
try {
|
|
56
|
+
parsed = JSON.parse(text);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return { profiles: {} };
|
|
60
|
+
}
|
|
61
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
62
|
+
return { profiles: {} };
|
|
63
|
+
}
|
|
64
|
+
const obj = parsed;
|
|
65
|
+
if (!obj["profiles"] || typeof obj["profiles"] !== "object") {
|
|
66
|
+
return { profiles: {} };
|
|
67
|
+
}
|
|
68
|
+
const rawProfiles = obj["profiles"];
|
|
69
|
+
const profiles = {};
|
|
70
|
+
for (const [name, raw] of Object.entries(rawProfiles)) {
|
|
71
|
+
if (raw === null || typeof raw !== "object")
|
|
72
|
+
continue;
|
|
73
|
+
const p = raw;
|
|
74
|
+
const profile = {};
|
|
75
|
+
if (typeof p["url"] === "string")
|
|
76
|
+
profile.url = p["url"];
|
|
77
|
+
if (typeof p["api_key"] === "string")
|
|
78
|
+
profile.apiKey = p["api_key"];
|
|
79
|
+
profiles[name] = profile;
|
|
80
|
+
}
|
|
81
|
+
const currentProfile = typeof obj["current_profile"] === "string"
|
|
82
|
+
? obj["current_profile"]
|
|
83
|
+
: undefined;
|
|
84
|
+
return {
|
|
85
|
+
currentProfile: currentProfile && profiles[currentProfile] !== undefined
|
|
86
|
+
? currentProfile
|
|
87
|
+
: undefined,
|
|
88
|
+
profiles,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/** Serialise a Store to the CLI's on-disk JSON shape (snake_case fields). */
|
|
92
|
+
function serialize(store) {
|
|
93
|
+
const profilesOut = {};
|
|
94
|
+
for (const [name, p] of Object.entries(store.profiles)) {
|
|
95
|
+
const o = {};
|
|
96
|
+
if (p.url !== undefined)
|
|
97
|
+
o["url"] = p.url;
|
|
98
|
+
if (p.apiKey !== undefined)
|
|
99
|
+
o["api_key"] = p.apiKey;
|
|
100
|
+
profilesOut[name] = o;
|
|
101
|
+
}
|
|
102
|
+
const body = { profiles: profilesOut };
|
|
103
|
+
if (store.currentProfile !== undefined) {
|
|
104
|
+
body["current_profile"] = store.currentProfile;
|
|
105
|
+
}
|
|
106
|
+
return JSON.stringify(body, null, 2) + "\n";
|
|
107
|
+
}
|
|
108
|
+
/** Upsert one profile and persist (mode 0600). Used by auto-register. */
|
|
109
|
+
function upsertProfile(name, patch, setCurrent) {
|
|
110
|
+
const store = readStore();
|
|
111
|
+
const merged = { ...(store.profiles[name] ?? {}), ...patch };
|
|
112
|
+
store.profiles[name] = merged;
|
|
113
|
+
if (setCurrent || store.currentProfile === undefined) {
|
|
114
|
+
store.currentProfile = name;
|
|
115
|
+
}
|
|
116
|
+
const path = storePath();
|
|
117
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
118
|
+
writeFileSync(path, serialize(store), { mode: 0o600 });
|
|
119
|
+
chmodSync(path, 0o600);
|
|
120
|
+
}
|
|
121
|
+
/** Resolve the relay URL using the same precedence as the CLI. */
|
|
122
|
+
export function resolveUrl() {
|
|
123
|
+
const store = readStore();
|
|
124
|
+
const active = store.currentProfile
|
|
125
|
+
? store.profiles[store.currentProfile]
|
|
126
|
+
: undefined;
|
|
127
|
+
const url = process.env.PANE_URL ?? active?.url ?? DEFAULT_RELAY_URL;
|
|
128
|
+
return url.replace(/\/$/, "");
|
|
129
|
+
}
|
|
130
|
+
/** Resolve the API key (env → PANE_TOKEN alias → active profile). */
|
|
131
|
+
function resolveApiKey() {
|
|
132
|
+
const store = readStore();
|
|
133
|
+
const active = store.currentProfile
|
|
134
|
+
? store.profiles[store.currentProfile]
|
|
135
|
+
: undefined;
|
|
136
|
+
const key = process.env.PANE_API_KEY ?? process.env.PANE_TOKEN ?? active?.apiKey;
|
|
137
|
+
return key && key !== "" ? key : undefined;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Resolve a ready-to-use PaneClient.
|
|
141
|
+
*
|
|
142
|
+
* First-run setup: if no API key is resolvable from the environment or the
|
|
143
|
+
* shared store, the server auto-registers a fresh agent against the relay and
|
|
144
|
+
* persists the key under the `default` profile in the shared store — so the
|
|
145
|
+
* CLI and any later MCP launch reuse the same identity, and the human never
|
|
146
|
+
* has to run `pane agent register` by hand.
|
|
147
|
+
*
|
|
148
|
+
* A self-hoster on a `secret`-mode relay (or anyone who prefers explicit
|
|
149
|
+
* provisioning) sets PANE_API_KEY / PANE_TOKEN and the auto-register path is
|
|
150
|
+
* never taken.
|
|
151
|
+
*
|
|
152
|
+
* `opts.agentName` labels the auto-registered agent on the relay.
|
|
153
|
+
* `opts.registerSecret` is forwarded as the registration secret for
|
|
154
|
+
* REGISTRATION_MODE=secret relays.
|
|
155
|
+
*/
|
|
156
|
+
export async function resolveClient(opts = {}) {
|
|
157
|
+
const url = resolveUrl();
|
|
158
|
+
let apiKey = resolveApiKey();
|
|
159
|
+
if (apiKey === undefined) {
|
|
160
|
+
// No key anywhere — provision one and persist it under `default`.
|
|
161
|
+
const result = await registerAgent({
|
|
162
|
+
url,
|
|
163
|
+
name: opts.agentName ?? "pane-mcp",
|
|
164
|
+
...(opts.registerSecret !== undefined && opts.registerSecret !== ""
|
|
165
|
+
? { secret: opts.registerSecret }
|
|
166
|
+
: {}),
|
|
167
|
+
});
|
|
168
|
+
upsertProfile(DEFAULT_PROFILE_NAME, { url, apiKey: result.api_key }, true);
|
|
169
|
+
apiKey = result.api_key;
|
|
170
|
+
}
|
|
171
|
+
return new PaneClient({ url, apiKey });
|
|
172
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `pane-mcp` — a thin stdio Model Context Protocol server wrapping Pane.
|
|
3
|
+
//
|
|
4
|
+
// Speaks MCP over stdio so any MCP client (Claude Desktop, Cursor, …) can use
|
|
5
|
+
// Pane: create panes, push updates, and poll for the human's response. All
|
|
6
|
+
// relay I/O goes through @paneui/core (no duplicated transport logic), and
|
|
7
|
+
// config is shared with the `pane` CLI (~/.config/pane/config.json) — so the
|
|
8
|
+
// CLI and this server use the same agent identity.
|
|
9
|
+
//
|
|
10
|
+
// Config (all optional — sensible defaults; auto-registers an agent on first
|
|
11
|
+
// use if no key is found):
|
|
12
|
+
// PANE_URL relay base URL (default https://relay.paneui.com)
|
|
13
|
+
// PANE_API_KEY agent API key (or use the shared CLI store)
|
|
14
|
+
// PANE_TOKEN alias for PANE_API_KEY (for MCP host "*_TOKEN" config)
|
|
15
|
+
// PANE_AGENT_NAME label for the auto-registered agent
|
|
16
|
+
// PANE_REGISTER_SECRET registration secret (REGISTRATION_MODE=secret relays)
|
|
17
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
18
|
+
import { buildServer } from "./server.js";
|
|
19
|
+
import { VERSION } from "./version.js";
|
|
20
|
+
async function main() {
|
|
21
|
+
// --version / --help are answered locally without starting the transport, so
|
|
22
|
+
// a human poking at the binary gets a useful response instead of a hung
|
|
23
|
+
// stdio session waiting for JSON-RPC.
|
|
24
|
+
const argv = process.argv.slice(2);
|
|
25
|
+
if (argv.includes("--version") || argv.includes("-v")) {
|
|
26
|
+
process.stdout.write(`pane-mcp ${VERSION}\n`);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
30
|
+
process.stdout.write(HELP);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const server = buildServer({
|
|
34
|
+
agentName: process.env.PANE_AGENT_NAME,
|
|
35
|
+
registerSecret: process.env.PANE_REGISTER_SECRET,
|
|
36
|
+
});
|
|
37
|
+
const transport = new StdioServerTransport();
|
|
38
|
+
await server.connect(transport);
|
|
39
|
+
// Stdio MCP servers run until the host closes stdin; keep the process alive.
|
|
40
|
+
// The transport resolves connect() immediately, so without this the event
|
|
41
|
+
// loop would otherwise stay open only because of the stdin reader — which is
|
|
42
|
+
// the intended behaviour. Nothing more to do here.
|
|
43
|
+
}
|
|
44
|
+
const HELP = `pane-mcp ${VERSION} — Pane Model Context Protocol server (stdio)
|
|
45
|
+
|
|
46
|
+
Run by an MCP client over stdio; not meant to be invoked interactively. Add it
|
|
47
|
+
to your MCP client config, e.g. Claude Desktop / Cursor:
|
|
48
|
+
|
|
49
|
+
{
|
|
50
|
+
"mcpServers": {
|
|
51
|
+
"pane": {
|
|
52
|
+
"command": "npx",
|
|
53
|
+
"args": ["-y", "@paneui/mcp"],
|
|
54
|
+
"env": { "PANE_API_KEY": "pane_..." }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
Environment:
|
|
60
|
+
PANE_URL Relay base URL (default https://relay.paneui.com)
|
|
61
|
+
PANE_API_KEY Agent API key. If unset, the server auto-registers an
|
|
62
|
+
PANE_TOKEN agent on first use and saves the key to the shared CLI
|
|
63
|
+
store (~/.config/pane/config.json). PANE_TOKEN is an
|
|
64
|
+
alias for PANE_API_KEY.
|
|
65
|
+
PANE_AGENT_NAME Display name for the auto-registered agent.
|
|
66
|
+
PANE_REGISTER_SECRET Registration secret (REGISTRATION_MODE=secret relays).
|
|
67
|
+
|
|
68
|
+
Tools exposed: create_pane, get_pane_state, get_events, send_to_pane,
|
|
69
|
+
list_records, upsert_record, update_record, delete_record.
|
|
70
|
+
|
|
71
|
+
See https://github.com/aerolalit/paneui for docs.
|
|
72
|
+
`;
|
|
73
|
+
main().catch((e) => {
|
|
74
|
+
process.stderr.write(`pane-mcp: fatal: ${e instanceof Error ? (e.stack ?? e.message) : String(e)}\n`);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
});
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Builds the Pane MCP server: registers every tool from ./tools.ts against an
|
|
2
|
+
// McpServer and wires each handler to a lazily-resolved PaneClient.
|
|
3
|
+
//
|
|
4
|
+
// The PaneClient is resolved ONCE, on the first tool call, then cached — so the
|
|
5
|
+
// (potentially network-touching) auto-register-on-first-use path runs lazily,
|
|
6
|
+
// not at process start. This keeps `initialize` / `tools/list` fast and offline
|
|
7
|
+
// (an MCP host can enumerate the tools without the relay being reachable), and
|
|
8
|
+
// only the first actual tool call provisions a key if needed.
|
|
9
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
10
|
+
import { resolveClient } from "./config.js";
|
|
11
|
+
import { TOOLS } from "./tools.js";
|
|
12
|
+
import { VERSION } from "./version.js";
|
|
13
|
+
/**
|
|
14
|
+
* Construct (but do not connect) the Pane MCP server. Call `.connect(transport)`
|
|
15
|
+
* on the returned server to start serving.
|
|
16
|
+
*/
|
|
17
|
+
export function buildServer(opts = {}) {
|
|
18
|
+
const server = new McpServer({
|
|
19
|
+
name: "pane",
|
|
20
|
+
version: VERSION,
|
|
21
|
+
});
|
|
22
|
+
// Lazily resolve + memoise the client. A failed resolution is not cached, so
|
|
23
|
+
// a transient error (e.g. relay unreachable during auto-register) can be
|
|
24
|
+
// retried on the next tool call.
|
|
25
|
+
let clientPromise;
|
|
26
|
+
const getClient = () => {
|
|
27
|
+
if (opts.client)
|
|
28
|
+
return Promise.resolve(opts.client);
|
|
29
|
+
if (clientPromise === undefined) {
|
|
30
|
+
clientPromise = resolveClient({
|
|
31
|
+
agentName: opts.agentName,
|
|
32
|
+
registerSecret: opts.registerSecret,
|
|
33
|
+
}).catch((e) => {
|
|
34
|
+
clientPromise = undefined;
|
|
35
|
+
throw e;
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return clientPromise;
|
|
39
|
+
};
|
|
40
|
+
for (const tool of TOOLS) {
|
|
41
|
+
server.registerTool(tool.name, {
|
|
42
|
+
description: tool.description,
|
|
43
|
+
inputSchema: tool.inputSchema,
|
|
44
|
+
}, async (args) => {
|
|
45
|
+
let client;
|
|
46
|
+
try {
|
|
47
|
+
client = await getClient();
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
51
|
+
return {
|
|
52
|
+
content: [
|
|
53
|
+
{
|
|
54
|
+
type: "text",
|
|
55
|
+
text: JSON.stringify({
|
|
56
|
+
error: "config_error",
|
|
57
|
+
message,
|
|
58
|
+
hint: "Set PANE_API_KEY (or PANE_TOKEN), or ensure the relay at PANE_URL is reachable so the server can auto-register an agent.",
|
|
59
|
+
}, null, 2),
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
isError: true,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return tool.handler(client, args);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return server;
|
|
69
|
+
}
|
package/dist/tools.js
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
// Tool definitions for the Pane MCP server.
|
|
2
|
+
//
|
|
3
|
+
// Each tool wraps a @paneui/core PaneClient operation. The descriptions are
|
|
4
|
+
// written for the LLM consumer — they ARE the docs the model reads to decide
|
|
5
|
+
// when and how to call each tool. Keep them concrete and action-oriented.
|
|
6
|
+
//
|
|
7
|
+
// Design notes:
|
|
8
|
+
// - MCP tools are request/response. There is no long-lived "watch" — instead
|
|
9
|
+
// `get_events` is a poll: the model calls it with the cursor from the last
|
|
10
|
+
// call until the awaited event appears. Each description spells out the
|
|
11
|
+
// poll loop so the model drives it correctly.
|
|
12
|
+
// - Schema validation is done with Zod raw shapes (the shape the MCP SDK's
|
|
13
|
+
// registerTool expects); the SDK validates arguments against them before
|
|
14
|
+
// the handler runs, so handlers receive typed, validated input.
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
import { PaneApiError } from "@paneui/core";
|
|
17
|
+
/** Wrap a JSON-able value as a single text-content tool result. */
|
|
18
|
+
function jsonResult(value) {
|
|
19
|
+
return {
|
|
20
|
+
content: [{ type: "text", text: JSON.stringify(value, null, 2) }],
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Turn any thrown error into a structured `isError` tool result. PaneApiError
|
|
25
|
+
* carries the relay's `code`, HTTP `status`, and an optional remediation
|
|
26
|
+
* `hint`; surface all of it so the model can self-correct (e.g. fix an event
|
|
27
|
+
* type the schema rejected) instead of getting an opaque failure.
|
|
28
|
+
*/
|
|
29
|
+
function errorResult(e) {
|
|
30
|
+
if (e instanceof PaneApiError) {
|
|
31
|
+
const payload = {
|
|
32
|
+
error: e.code,
|
|
33
|
+
status: e.status,
|
|
34
|
+
message: e.message,
|
|
35
|
+
};
|
|
36
|
+
if (e.hint)
|
|
37
|
+
payload["hint"] = e.hint;
|
|
38
|
+
if (e.details !== undefined)
|
|
39
|
+
payload["details"] = e.details;
|
|
40
|
+
if (e.retryable !== undefined)
|
|
41
|
+
payload["retryable"] = e.retryable;
|
|
42
|
+
return {
|
|
43
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
|
|
44
|
+
isError: true,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
48
|
+
return {
|
|
49
|
+
content: [
|
|
50
|
+
{
|
|
51
|
+
type: "text",
|
|
52
|
+
text: JSON.stringify({ error: "internal", message }, null, 2),
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
isError: true,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// Tool input schemas
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
const createPaneShape = {
|
|
62
|
+
name: z
|
|
63
|
+
.string()
|
|
64
|
+
.min(1)
|
|
65
|
+
.describe("Short human-readable label for the auto-created template, shown in the owner UI (e.g. 'Deploy approval', 'Vendor picker')."),
|
|
66
|
+
html: z
|
|
67
|
+
.string()
|
|
68
|
+
.min(1)
|
|
69
|
+
.describe("The pane's UI as a complete inline HTML document. To send data back to you, the page calls window.pane.emit(eventType, payload) — every emitted eventType MUST be declared in event_schema below with 'page' in its emittedBy. Read window.pane.inputData for seed data passed via input_data."),
|
|
70
|
+
event_schema: z
|
|
71
|
+
.record(z.string(), z.unknown())
|
|
72
|
+
.optional()
|
|
73
|
+
.describe("Optional. Declares which events the page (and you) may emit and validates each payload. Shape: { events: { '<type>': { emittedBy: ['page'|'agent'...], payload: <JSON Schema> } } }. OMIT for a read-only pane (dashboard/status view the human only looks at — it then accepts no events)."),
|
|
74
|
+
input_data: z
|
|
75
|
+
.record(z.string(), z.unknown())
|
|
76
|
+
.optional()
|
|
77
|
+
.describe("Optional seed data for this pane instance, readable in the page as window.pane.inputData (e.g. the diff to review, the options to pick from)."),
|
|
78
|
+
input_schema: z
|
|
79
|
+
.record(z.string(), z.unknown())
|
|
80
|
+
.optional()
|
|
81
|
+
.describe("Optional JSON Schema validating input_data. Only needed if input_data references uploaded attachment ids the page must download."),
|
|
82
|
+
title: z
|
|
83
|
+
.string()
|
|
84
|
+
.optional()
|
|
85
|
+
.describe("Optional browser tab title for the human (≤80 chars). Defaults to `name`."),
|
|
86
|
+
preamble: z
|
|
87
|
+
.string()
|
|
88
|
+
.max(300)
|
|
89
|
+
.optional()
|
|
90
|
+
.describe("Optional one/two-line context shown above the UI — 'who is asking, and why'. Use it whenever the pane isn't self-explanatory."),
|
|
91
|
+
ttl_seconds: z
|
|
92
|
+
.number()
|
|
93
|
+
.int()
|
|
94
|
+
.positive()
|
|
95
|
+
.optional()
|
|
96
|
+
.describe("Optional pane lifetime in seconds. The relay clamps to its max; the returned expires_at is authoritative."),
|
|
97
|
+
context_key: z
|
|
98
|
+
.string()
|
|
99
|
+
.min(1)
|
|
100
|
+
.max(256)
|
|
101
|
+
.optional()
|
|
102
|
+
.describe("Optional natural key (e.g. 'pr-42', 'deal-1138'). Repeated create_pane calls with the same (template, key) return the SAME pane instead of a new one — use it to make retries idempotent."),
|
|
103
|
+
};
|
|
104
|
+
const getPaneStateShape = {
|
|
105
|
+
pane_id: z.string().min(1).describe("The pane id returned by create_pane."),
|
|
106
|
+
};
|
|
107
|
+
const getEventsShape = {
|
|
108
|
+
pane_id: z.string().min(1).describe("The pane id to read events from."),
|
|
109
|
+
since: z
|
|
110
|
+
.string()
|
|
111
|
+
.optional()
|
|
112
|
+
.describe("Opaque cursor from a previous get_events call's next_cursor. Omit on the first call to read from the beginning."),
|
|
113
|
+
wait_seconds: z
|
|
114
|
+
.number()
|
|
115
|
+
.int()
|
|
116
|
+
.min(0)
|
|
117
|
+
.max(30)
|
|
118
|
+
.optional()
|
|
119
|
+
.describe("Optional long-poll: how long the relay holds the request open waiting for a new event (0–30s, relay-capped). Use ~25 when waiting for a human to act, so each poll either returns promptly with the event or returns empty and you call again with the same cursor."),
|
|
120
|
+
};
|
|
121
|
+
const sendToPaneShape = {
|
|
122
|
+
pane_id: z.string().min(1).describe("The pane id to push the event into."),
|
|
123
|
+
type: z
|
|
124
|
+
.string()
|
|
125
|
+
.min(1)
|
|
126
|
+
.describe("Event type. Must be declared in the pane's event_schema with 'agent' in its emittedBy list (the page sees it live)."),
|
|
127
|
+
data: z
|
|
128
|
+
.unknown()
|
|
129
|
+
.describe("Event payload — any JSON value valid against the type's payload schema. Use {} or null for a no-payload event."),
|
|
130
|
+
idempotency_key: z
|
|
131
|
+
.string()
|
|
132
|
+
.optional()
|
|
133
|
+
.describe("Optional dedup key — a repeat send with the same key is a no-op."),
|
|
134
|
+
};
|
|
135
|
+
const listRecordsShape = {
|
|
136
|
+
pane_id: z.string().min(1).describe("The pane id."),
|
|
137
|
+
collection: z
|
|
138
|
+
.string()
|
|
139
|
+
.min(1)
|
|
140
|
+
.describe("The record collection name declared in the pane's record schema."),
|
|
141
|
+
since: z
|
|
142
|
+
.number()
|
|
143
|
+
.int()
|
|
144
|
+
.optional()
|
|
145
|
+
.describe("Optional cursor (next_since from a prior call) for pagination."),
|
|
146
|
+
limit: z.number().int().positive().optional().describe("Optional page size."),
|
|
147
|
+
};
|
|
148
|
+
const upsertRecordShape = {
|
|
149
|
+
pane_id: z.string().min(1).describe("The pane id."),
|
|
150
|
+
collection: z.string().min(1).describe("The record collection name."),
|
|
151
|
+
record_key: z
|
|
152
|
+
.string()
|
|
153
|
+
.optional()
|
|
154
|
+
.describe("Optional stable key for this record. Reusing an existing key returns the existing row (deduped:true) rather than creating a duplicate; omit to let the relay assign one."),
|
|
155
|
+
data: z
|
|
156
|
+
.unknown()
|
|
157
|
+
.describe("The record body — any JSON value valid against the collection schema."),
|
|
158
|
+
};
|
|
159
|
+
const updateRecordShape = {
|
|
160
|
+
pane_id: z.string().min(1).describe("The pane id."),
|
|
161
|
+
collection: z.string().min(1).describe("The record collection name."),
|
|
162
|
+
record_key: z.string().min(1).describe("The key of the record to update."),
|
|
163
|
+
data: z.unknown().describe("The new record body (replaces the row's data)."),
|
|
164
|
+
if_match: z
|
|
165
|
+
.number()
|
|
166
|
+
.int()
|
|
167
|
+
.optional()
|
|
168
|
+
.describe("Optional optimistic-lock version. If it doesn't match the current row, the update is rejected with the current row in details.current."),
|
|
169
|
+
};
|
|
170
|
+
const deleteRecordShape = {
|
|
171
|
+
pane_id: z.string().min(1).describe("The pane id."),
|
|
172
|
+
collection: z.string().min(1).describe("The record collection name."),
|
|
173
|
+
record_key: z.string().min(1).describe("The key of the record to delete."),
|
|
174
|
+
};
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
// Tool definitions
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
export const TOOLS = [
|
|
179
|
+
{
|
|
180
|
+
name: "create_pane",
|
|
181
|
+
description: "Hand the human a rich interactive UI by URL and (optionally) get structured data back. Build the UI as inline HTML; the relay hosts it and returns a URL. ALWAYS give the returned url (result.url) to the human — paste it into the conversation and ask them to open it. Reach for this whenever a text reply is the wrong shape: forms, approvals, pickers, surveys, dashboards, diff/doc review, multi-step wizards. If the page captures input it emits events back to you (poll them with get_events). A read-only dashboard with no event_schema is valid too. Returns { pane_id, url, expires_at }.",
|
|
182
|
+
inputSchema: createPaneShape,
|
|
183
|
+
handler: async (client, args) => {
|
|
184
|
+
try {
|
|
185
|
+
const template = {
|
|
186
|
+
name: args["name"],
|
|
187
|
+
type: "html-inline",
|
|
188
|
+
source: args["html"],
|
|
189
|
+
};
|
|
190
|
+
if (args["event_schema"] !== undefined)
|
|
191
|
+
template["event_schema"] = args["event_schema"];
|
|
192
|
+
if (args["input_schema"] !== undefined)
|
|
193
|
+
template["input_schema"] = args["input_schema"];
|
|
194
|
+
const req = { template };
|
|
195
|
+
if (args["input_data"] !== undefined)
|
|
196
|
+
req["input_data"] = args["input_data"];
|
|
197
|
+
if (args["title"] !== undefined)
|
|
198
|
+
req["title"] = args["title"];
|
|
199
|
+
if (args["preamble"] !== undefined)
|
|
200
|
+
req["preamble"] = args["preamble"];
|
|
201
|
+
if (args["ttl_seconds"] !== undefined)
|
|
202
|
+
req["ttl"] = args["ttl_seconds"];
|
|
203
|
+
if (args["context_key"] !== undefined)
|
|
204
|
+
req["context_key"] = args["context_key"];
|
|
205
|
+
const res = await client.createPane(req);
|
|
206
|
+
const humanUrl = res.urls.humans[0] ?? null;
|
|
207
|
+
return jsonResult({
|
|
208
|
+
pane_id: res.pane_id,
|
|
209
|
+
// The single URL to deliver to the human. (urls.humans carries all
|
|
210
|
+
// of them when participants > 1.)
|
|
211
|
+
url: humanUrl,
|
|
212
|
+
urls: res.urls.humans,
|
|
213
|
+
title: res.title,
|
|
214
|
+
expires_at: res.expires_at,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
catch (e) {
|
|
218
|
+
return errorResult(e);
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
name: "get_pane_state",
|
|
224
|
+
description: "Fetch a pane's current metadata (status, title, template version, timestamps, expires_at) WITHOUT its event log. Use it to check whether a pane is still open or has expired. To read what the human did, use get_events.",
|
|
225
|
+
inputSchema: getPaneStateShape,
|
|
226
|
+
handler: async (client, args) => {
|
|
227
|
+
try {
|
|
228
|
+
const state = await client.getPane(String(args["pane_id"]));
|
|
229
|
+
return jsonResult(state);
|
|
230
|
+
}
|
|
231
|
+
catch (e) {
|
|
232
|
+
return errorResult(e);
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
name: "get_events",
|
|
238
|
+
description: "Poll a pane's append-only event log for what the human did (form submissions, approvals, picks). This is how you receive the round-trip result — there is no push/streaming in MCP. Poll loop: call with no `since` first; process the returned events; remember next_cursor; call again passing it as `since` to get only newer events. To WAIT for a human who hasn't acted yet, pass wait_seconds (~25) so the relay holds the request open until an event arrives or it times out, then call again with the same cursor. Returns { events, next_cursor }.",
|
|
239
|
+
inputSchema: getEventsShape,
|
|
240
|
+
handler: async (client, args) => {
|
|
241
|
+
try {
|
|
242
|
+
const page = await client.getEvents(String(args["pane_id"]), {
|
|
243
|
+
since: args["since"],
|
|
244
|
+
waitSeconds: args["wait_seconds"],
|
|
245
|
+
});
|
|
246
|
+
return jsonResult(page);
|
|
247
|
+
}
|
|
248
|
+
catch (e) {
|
|
249
|
+
return errorResult(e);
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
name: "send_to_pane",
|
|
255
|
+
description: "Push an event INTO an open pane — update the live UI the human is looking at (progress, a new message, a status change, fresh data). The event type must be declared in the pane's event_schema with 'agent' in its emittedBy. For mutable collections (todos, line items, comment threads) prefer the record tools instead. Returns { event, deduped }.",
|
|
256
|
+
inputSchema: sendToPaneShape,
|
|
257
|
+
handler: async (client, args) => {
|
|
258
|
+
try {
|
|
259
|
+
const res = await client.sendEvent(String(args["pane_id"]), {
|
|
260
|
+
type: String(args["type"]),
|
|
261
|
+
data: args["data"],
|
|
262
|
+
idempotencyKey: args["idempotency_key"],
|
|
263
|
+
});
|
|
264
|
+
return jsonResult(res);
|
|
265
|
+
}
|
|
266
|
+
catch (e) {
|
|
267
|
+
return errorResult(e);
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
name: "list_records",
|
|
273
|
+
description: "List rows in a pane's mutable record collection (e.g. a todo list, shopping list, kanban board, comment thread). Records are the right primitive when the page shows several mutable items and the CURRENT state matters more than the history of edits. Includes tombstones (deleted_at set) so you can observe deletions. Returns { records, next_since, has_more }.",
|
|
274
|
+
inputSchema: listRecordsShape,
|
|
275
|
+
handler: async (client, args) => {
|
|
276
|
+
try {
|
|
277
|
+
const out = await client.listRecords(String(args["pane_id"]), String(args["collection"]), {
|
|
278
|
+
since: args["since"],
|
|
279
|
+
limit: args["limit"],
|
|
280
|
+
});
|
|
281
|
+
return jsonResult(out);
|
|
282
|
+
}
|
|
283
|
+
catch (e) {
|
|
284
|
+
return errorResult(e);
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
name: "upsert_record",
|
|
290
|
+
description: "Create a row in a pane's record collection, or return the existing row if record_key is already present (deduped:true). Use to add a todo, a line item, a comment, etc. The collection must be declared in the pane's record schema with 'agent' allowed to write. Returns { record, deduped }.",
|
|
291
|
+
inputSchema: upsertRecordShape,
|
|
292
|
+
handler: async (client, args) => {
|
|
293
|
+
try {
|
|
294
|
+
const body = {
|
|
295
|
+
data: args["data"],
|
|
296
|
+
};
|
|
297
|
+
if (args["record_key"] !== undefined)
|
|
298
|
+
body.record_key = String(args["record_key"]);
|
|
299
|
+
const out = await client.upsertRecord(String(args["pane_id"]), String(args["collection"]), body);
|
|
300
|
+
return jsonResult(out);
|
|
301
|
+
}
|
|
302
|
+
catch (e) {
|
|
303
|
+
return errorResult(e);
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
name: "update_record",
|
|
309
|
+
description: "Update an existing row in a pane's record collection (replaces its data). Pass if_match with the row's current version for an optimistic-locked update — on a version mismatch the relay returns the current row so you can retry. Returns { record }.",
|
|
310
|
+
inputSchema: updateRecordShape,
|
|
311
|
+
handler: async (client, args) => {
|
|
312
|
+
try {
|
|
313
|
+
const body = {
|
|
314
|
+
data: args["data"],
|
|
315
|
+
};
|
|
316
|
+
if (args["if_match"] !== undefined)
|
|
317
|
+
body.if_match = args["if_match"];
|
|
318
|
+
const out = await client.updateRecord(String(args["pane_id"]), String(args["collection"]), String(args["record_key"]), body);
|
|
319
|
+
return jsonResult(out);
|
|
320
|
+
}
|
|
321
|
+
catch (e) {
|
|
322
|
+
return errorResult(e);
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
name: "delete_record",
|
|
328
|
+
description: "Soft-delete a row from a pane's record collection. The page sees the deletion live (the row becomes a tombstone in list_records). Returns { deleted: true }.",
|
|
329
|
+
inputSchema: deleteRecordShape,
|
|
330
|
+
handler: async (client, args) => {
|
|
331
|
+
try {
|
|
332
|
+
await client.deleteRecord(String(args["pane_id"]), String(args["collection"]), String(args["record_key"]));
|
|
333
|
+
return jsonResult({ deleted: true });
|
|
334
|
+
}
|
|
335
|
+
catch (e) {
|
|
336
|
+
return errorResult(e);
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
];
|
package/dist/version.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@paneui/mcp",
|
|
3
|
+
"version": "0.0.19",
|
|
4
|
+
"description": "Model Context Protocol (stdio) server for Pane: lets any MCP client (Claude Desktop, Cursor, …) hand a human a rich interactive UI by URL and get structured data back.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"pane",
|
|
9
|
+
"mcp",
|
|
10
|
+
"model-context-protocol",
|
|
11
|
+
"agent",
|
|
12
|
+
"relay",
|
|
13
|
+
"human-in-the-loop"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://github.com/aerolalit/paneui#readme",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/aerolalit/paneui.git",
|
|
19
|
+
"directory": "packages/mcp"
|
|
20
|
+
},
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/aerolalit/paneui/issues"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=20"
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"bin": {
|
|
31
|
+
"pane-mcp": "dist/index.js"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist",
|
|
35
|
+
"server.json",
|
|
36
|
+
"LICENSE",
|
|
37
|
+
"README.md"
|
|
38
|
+
],
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsc",
|
|
41
|
+
"typecheck": "tsc --noEmit",
|
|
42
|
+
"test": "vitest run",
|
|
43
|
+
"test:unit": "vitest run"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@modelcontextprotocol/sdk": "^1.20.0",
|
|
47
|
+
"@paneui/core": "^0.0.19",
|
|
48
|
+
"zod": "^4.4.3"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/node": "^25.9.2",
|
|
52
|
+
"typescript": "^6.0.3",
|
|
53
|
+
"vitest": "^4.1.8"
|
|
54
|
+
}
|
|
55
|
+
}
|
package/server.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
+
"name": "io.github.aerolalit/pane",
|
|
4
|
+
"title": "Pane",
|
|
5
|
+
"description": "Hand a human a rich interactive UI by URL and get structured data back — forms, approvals, pickers, dashboards, diff review — from any MCP client.",
|
|
6
|
+
"version": "0.0.18",
|
|
7
|
+
"repository": {
|
|
8
|
+
"url": "https://github.com/aerolalit/paneui",
|
|
9
|
+
"source": "github"
|
|
10
|
+
},
|
|
11
|
+
"packages": [
|
|
12
|
+
{
|
|
13
|
+
"registryType": "npm",
|
|
14
|
+
"registryBaseUrl": "https://registry.npmjs.org",
|
|
15
|
+
"identifier": "@paneui/mcp",
|
|
16
|
+
"version": "0.0.18",
|
|
17
|
+
"transport": {
|
|
18
|
+
"type": "stdio"
|
|
19
|
+
},
|
|
20
|
+
"environmentVariables": [
|
|
21
|
+
{
|
|
22
|
+
"name": "PANE_API_KEY",
|
|
23
|
+
"description": "Pane agent API key. If unset, the server auto-registers a fresh agent against the relay on first use and saves the key to the shared CLI store (~/.config/pane/config.json).",
|
|
24
|
+
"isRequired": false,
|
|
25
|
+
"isSecret": true
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"name": "PANE_URL",
|
|
29
|
+
"description": "Pane relay base URL. Defaults to the hosted relay https://relay.paneui.com; set this to point at a self-hosted relay.",
|
|
30
|
+
"isRequired": false,
|
|
31
|
+
"isSecret": false
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"name": "PANE_AGENT_NAME",
|
|
35
|
+
"description": "Display name for the auto-registered agent on the relay.",
|
|
36
|
+
"isRequired": false,
|
|
37
|
+
"isSecret": false
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"name": "PANE_REGISTER_SECRET",
|
|
41
|
+
"description": "Registration secret, required only for relays running REGISTRATION_MODE=secret.",
|
|
42
|
+
"isRequired": false,
|
|
43
|
+
"isSecret": true
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
}
|