@paneui/mcp 0.0.23 → 0.0.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,9 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare const GUIDE_RESOURCE_URI = "pane://guide";
3
+ export declare const GUIDE_PROMPT_NAME = "pane_guide";
4
+ /**
5
+ * Register the `pane_guide` prompt and the `pane://guide` resource on `server`.
6
+ * `getGuide()` returns the current MCP-flavoured guide markdown (called lazily
7
+ * on each read so a relay can serve an updated guide without re-registering).
8
+ */
9
+ export declare function registerGuideCapabilities(server: McpServer, getGuide: () => string | Promise<string>): void;
@@ -0,0 +1,50 @@
1
+ // Register pane's MCP prompt + resource on an McpServer.
2
+ //
3
+ // Both the stdio server (packages/mcp/src/server.ts) and the relay's HTTP MCP
4
+ // server call this so an MCP-native client can discover the conceptual guide
5
+ // without a tool call:
6
+ //
7
+ // - prompt `pane_guide` — surfaces the guide as a prompt the client can
8
+ // insert into context ("teach me pane").
9
+ // - resource `pane://guide` — the same guide as a readable resource.
10
+ //
11
+ // The guide text is supplied by the host: the relay composes it in-process
12
+ // (MCP-INVOCATION.md + the core extracted from SKILL.md); the stdio server
13
+ // fetches it from the relay over HTTP and falls back to a short pointer when
14
+ // the relay is unreachable at registration time (registration must not block on
15
+ // the network — the get_skill tool is the always-fresh path).
16
+ export const GUIDE_RESOURCE_URI = "pane://guide";
17
+ export const GUIDE_PROMPT_NAME = "pane_guide";
18
+ /**
19
+ * Register the `pane_guide` prompt and the `pane://guide` resource on `server`.
20
+ * `getGuide()` returns the current MCP-flavoured guide markdown (called lazily
21
+ * on each read so a relay can serve an updated guide without re-registering).
22
+ */
23
+ export function registerGuideCapabilities(server, getGuide) {
24
+ server.registerResource(GUIDE_PROMPT_NAME, GUIDE_RESOURCE_URI, {
25
+ title: "Pane usage guide",
26
+ description: "The pane conceptual guide for MCP clients: when to use pane, events vs records, schema design, the house style, and the round-trip mental model — with MCP tool-call invocation grammar.",
27
+ mimeType: "text/markdown",
28
+ }, async () => {
29
+ const text = await getGuide();
30
+ return {
31
+ contents: [
32
+ { uri: GUIDE_RESOURCE_URI, mimeType: "text/markdown", text },
33
+ ],
34
+ };
35
+ });
36
+ server.registerPrompt(GUIDE_PROMPT_NAME, {
37
+ title: "Pane usage guide",
38
+ description: "Insert the pane usage guide (MCP invocation + conceptual core) into the conversation so the model knows how to drive pane's tools.",
39
+ }, async () => {
40
+ const text = await getGuide();
41
+ return {
42
+ messages: [
43
+ {
44
+ role: "user",
45
+ content: { type: "text", text },
46
+ },
47
+ ],
48
+ };
49
+ });
50
+ }
@@ -0,0 +1,65 @@
1
+ import { PaneClient } from "@paneui/core";
2
+ /**
3
+ * The hosted Pane relay — the URL fallback when nothing else is set. A
4
+ * self-hoster overrides it with PANE_URL or a registered profile.
5
+ */
6
+ export declare const DEFAULT_RELAY_URL = "https://relay.paneui.com";
7
+ /**
8
+ * Profile name used when this server auto-registers a fresh agent. Matches the
9
+ * CLI's DEFAULT_PROFILE_NAME so the two share the same default identity.
10
+ */
11
+ export declare const DEFAULT_PROFILE_NAME = "default";
12
+ /** Absolute path to the shared CLI/MCP config file (honours XDG_CONFIG_HOME). */
13
+ export declare function storePath(): string;
14
+ /**
15
+ * Clear the active saved profile from the shared store (mirrors `pane agent
16
+ * logout` for the active-profile case). Removes the profile entry and unsets
17
+ * `current_profile` so the next resolve falls back to env / the default URL.
18
+ * Local-only: it does NOT revoke the key on the relay (use the `key` tool's
19
+ * `revoke` action for that). Idempotent — clearing an empty store is a no-op.
20
+ * Returns the profile name that was cleared (or null when nothing was active)
21
+ * and the store path.
22
+ */
23
+ export declare function clearActiveProfile(): {
24
+ cleared: boolean;
25
+ profile: string | null;
26
+ path: string;
27
+ };
28
+ /** Resolve the relay URL using the same precedence as the CLI. */
29
+ export declare function resolveUrl(): string;
30
+ /**
31
+ * Describe how the server is currently configured WITHOUT touching the network
32
+ * — the resolved relay URL, the active profile name, where the key is coming
33
+ * from, and whether a key is present at all. Backs the `agent` tool's `whoami`
34
+ * action so an MCP client can introspect its own identity / relay binding the
35
+ * way `pane config show` does for the CLI. No secrets are returned (the API key
36
+ * plaintext is never surfaced — only its source + whether it exists).
37
+ */
38
+ export declare function describeActiveConfig(): {
39
+ url: string;
40
+ profile: string | null;
41
+ api_key_present: boolean;
42
+ api_key_source: "env" | "profile" | "none";
43
+ store_path: string;
44
+ };
45
+ /**
46
+ * Resolve a ready-to-use PaneClient.
47
+ *
48
+ * First-run setup: if no API key is resolvable from the environment or the
49
+ * shared store, the server auto-registers a fresh agent against the relay and
50
+ * persists the key under the `default` profile in the shared store — so the
51
+ * CLI and any later MCP launch reuse the same identity, and the human never
52
+ * has to run `pane agent register` by hand.
53
+ *
54
+ * A self-hoster on a `secret`-mode relay (or anyone who prefers explicit
55
+ * provisioning) sets PANE_API_KEY / PANE_TOKEN and the auto-register path is
56
+ * never taken.
57
+ *
58
+ * `opts.agentName` labels the auto-registered agent on the relay.
59
+ * `opts.registerSecret` is forwarded as the registration secret for
60
+ * REGISTRATION_MODE=secret relays.
61
+ */
62
+ export declare function resolveClient(opts?: {
63
+ agentName?: string;
64
+ registerSecret?: string;
65
+ }): Promise<PaneClient>;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Extract every `<!-- pane:core:start -->…<!-- pane:core:end -->` block from a
3
+ * SKILL.md body, concatenated in document order (markers removed). Returns the
4
+ * transport-agnostic conceptual core with no CLI command grammar.
5
+ */
6
+ export declare function extractCore(skillMarkdown: string): string;
7
+ /**
8
+ * Build the full MCP guide: the MCP invocation layer followed by the shared
9
+ * conceptual core extracted from SKILL.md. `mcpInvocation` is the contents of
10
+ * skills/pane/MCP-INVOCATION.md; `skillMarkdown` is the contents of SKILL.md.
11
+ */
12
+ export declare function composeMcpGuide(mcpInvocation: string, skillMarkdown: string): string;
package/dist/guide.js ADDED
@@ -0,0 +1,49 @@
1
+ // Compose the MCP-flavoured pane guide from the shared conceptual core + the
2
+ // MCP invocation layer.
3
+ //
4
+ // Single source of truth: the conceptual core lives in skills/pane/SKILL.md
5
+ // between `<!-- pane:core:start -->` / `<!-- pane:core:end -->` markers (the
6
+ // CLI invocation grammar lives OUTSIDE those markers, so the CLI document and
7
+ // the MCP guide share the exact same prose for "when to use pane / events vs
8
+ // records / schema design / house style / the round-trip mental model"). The
9
+ // MCP invocation layer (tool-call grammar) lives in skills/pane/MCP-INVOCATION.md.
10
+ //
11
+ // The MCP guide = MCP-INVOCATION.md (with its trailing "the rest is the core"
12
+ // pointer) + every core block extracted from SKILL.md, in document order. No
13
+ // `pane ...` command grammar leaks into it.
14
+ //
15
+ // This is pure string manipulation so both the relay (which reads the files at
16
+ // boot and serves the result) and any other consumer can share one
17
+ // implementation without dragging in I/O.
18
+ const CORE_START = "<!-- pane:core:start -->";
19
+ const CORE_END = "<!-- pane:core:end -->";
20
+ /**
21
+ * Extract every `<!-- pane:core:start -->…<!-- pane:core:end -->` block from a
22
+ * SKILL.md body, concatenated in document order (markers removed). Returns the
23
+ * transport-agnostic conceptual core with no CLI command grammar.
24
+ */
25
+ export function extractCore(skillMarkdown) {
26
+ const blocks = [];
27
+ let cursor = 0;
28
+ for (;;) {
29
+ const start = skillMarkdown.indexOf(CORE_START, cursor);
30
+ if (start === -1)
31
+ break;
32
+ const afterStart = start + CORE_START.length;
33
+ const end = skillMarkdown.indexOf(CORE_END, afterStart);
34
+ if (end === -1)
35
+ break;
36
+ blocks.push(skillMarkdown.slice(afterStart, end).trim());
37
+ cursor = end + CORE_END.length;
38
+ }
39
+ return blocks.join("\n\n");
40
+ }
41
+ /**
42
+ * Build the full MCP guide: the MCP invocation layer followed by the shared
43
+ * conceptual core extracted from SKILL.md. `mcpInvocation` is the contents of
44
+ * skills/pane/MCP-INVOCATION.md; `skillMarkdown` is the contents of SKILL.md.
45
+ */
46
+ export function composeMcpGuide(mcpInvocation, skillMarkdown) {
47
+ const core = extractCore(skillMarkdown);
48
+ return `${mcpInvocation.trim()}\n\n${core}\n`;
49
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,18 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { PaneClient } from "@paneui/core";
3
+ export interface BuildServerOptions {
4
+ /** Display name for the auto-registered agent (when no key is configured). */
5
+ agentName?: string;
6
+ /** Registration secret for REGISTRATION_MODE=secret relays. */
7
+ registerSecret?: string;
8
+ /**
9
+ * Inject a pre-built client (tests). When set, the lazy resolver is skipped
10
+ * entirely and no network/store access happens.
11
+ */
12
+ client?: PaneClient;
13
+ }
14
+ /**
15
+ * Construct (but do not connect) the Pane MCP server. Call `.connect(transport)`
16
+ * on the returned server to start serving.
17
+ */
18
+ export declare function buildServer(opts?: BuildServerOptions): McpServer;
package/dist/server.js CHANGED
@@ -7,9 +7,11 @@
7
7
  // (an MCP host can enumerate the tools without the relay being reachable), and
8
8
  // only the first actual tool call provisions a key if needed.
9
9
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
10
- import { resolveClient } from "./config.js";
10
+ import { resolveClient, resolveUrl } from "./config.js";
11
11
  import { TOOLS } from "./tools.js";
12
12
  import { VERSION } from "./version.js";
13
+ import { fetchMcpGuide } from "./skill.js";
14
+ import { registerGuideCapabilities } from "./capabilities.js";
13
15
  /**
14
16
  * Construct (but do not connect) the Pane MCP server. Call `.connect(transport)`
15
17
  * on the returned server to start serving.
@@ -37,10 +39,37 @@ export function buildServer(opts = {}) {
37
39
  }
38
40
  return clientPromise;
39
41
  };
42
+ // MCP consumers get the MCP-flavoured guide (tool-call grammar), not the
43
+ // CLI-grammar SKILL.md. get_skill fetches /skills/pane/MCP.md from the
44
+ // configured relay; everything else keeps its CLI defaults (the stdio server
45
+ // reads identity from the shared CLI config store).
46
+ const toolEnv = {
47
+ getSkill: (versionOnly) => fetchMcpGuide(resolveUrl(), { version: versionOnly }),
48
+ };
49
+ // Conceptual guide as an MCP prompt + resource. Fetched from the relay lazily
50
+ // on read; a relay-unreachable read surfaces a short pointer to get_skill
51
+ // rather than failing registration.
52
+ registerGuideCapabilities(server, async () => {
53
+ try {
54
+ const { markdown } = await fetchMcpGuide(resolveUrl());
55
+ return markdown ?? "";
56
+ }
57
+ catch (e) {
58
+ const message = e instanceof Error ? e.message : String(e);
59
+ return ("# pane\n\nThe pane guide could not be fetched from the relay " +
60
+ `(${message}).\n\nCall the \`get_skill\` tool to retrieve it once the ` +
61
+ "relay is reachable.\n");
62
+ }
63
+ });
40
64
  for (const tool of TOOLS) {
41
65
  server.registerTool(tool.name, {
66
+ // `title` (top-level, display name) + `annotations` (the ToolAnnotations
67
+ // behavioural hints, which also carry a title) both flow into tools/list
68
+ // so MCP hosts / Anthropic's connector directory can classify the tool.
69
+ title: tool.annotations.title,
42
70
  description: tool.description,
43
71
  inputSchema: tool.inputSchema,
72
+ annotations: tool.annotations,
44
73
  }, async (args) => {
45
74
  let client;
46
75
  try {
@@ -62,7 +91,7 @@ export function buildServer(opts = {}) {
62
91
  isError: true,
63
92
  };
64
93
  }
65
- return tool.handler(client, args);
94
+ return tool.handler(client, args, toolEnv);
66
95
  });
67
96
  }
68
97
  return server;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * GET the relay's full SKILL.md markdown. `version: true` instead fetches just
3
+ * the relay's reported skill version (the "is my local copy stale?" probe).
4
+ * Throws on a non-2xx or network failure with a message the tool layer can
5
+ * surface.
6
+ */
7
+ export declare function fetchSkill(relayUrl: string, opts?: {
8
+ version?: boolean;
9
+ }): Promise<{
10
+ markdown?: string;
11
+ version?: string;
12
+ }>;
13
+ /**
14
+ * GET the relay's MCP-flavoured guide (the conceptual core + MCP tool-call
15
+ * invocation grammar) from GET /skills/pane/MCP.md, or just its version from
16
+ * GET /skills/pane/MCP.md/version. This is what an MCP consumer should read
17
+ * (not the CLI-grammar SKILL.md). Served unauthenticated, same as the skill.
18
+ */
19
+ export declare function fetchMcpGuide(relayUrl: string, opts?: {
20
+ version?: boolean;
21
+ }): Promise<{
22
+ markdown?: string;
23
+ version?: string;
24
+ }>;
package/dist/skill.js CHANGED
@@ -37,6 +37,34 @@ export async function fetchSkill(relayUrl, opts = {}) {
37
37
  const markdown = await res.text();
38
38
  return { markdown };
39
39
  }
40
+ /**
41
+ * GET the relay's MCP-flavoured guide (the conceptual core + MCP tool-call
42
+ * invocation grammar) from GET /skills/pane/MCP.md, or just its version from
43
+ * GET /skills/pane/MCP.md/version. This is what an MCP consumer should read
44
+ * (not the CLI-grammar SKILL.md). Served unauthenticated, same as the skill.
45
+ */
46
+ export async function fetchMcpGuide(relayUrl, opts = {}) {
47
+ const base = relayUrl.replace(/\/$/, "");
48
+ if (opts.version) {
49
+ const res = await fetchOrThrow(base + "/skills/pane/MCP.md/version");
50
+ let body;
51
+ try {
52
+ body = await res.json();
53
+ }
54
+ catch {
55
+ body = null;
56
+ }
57
+ const version = body !== null &&
58
+ typeof body === "object" &&
59
+ typeof body.version === "string"
60
+ ? body.version
61
+ : "0.0.0";
62
+ return { version };
63
+ }
64
+ const res = await fetchOrThrow(base + "/skills/pane/MCP.md");
65
+ const markdown = await res.text();
66
+ return { markdown };
67
+ }
40
68
  async function fetchOrThrow(url) {
41
69
  let res;
42
70
  try {
@@ -0,0 +1,51 @@
1
+ import { z } from "zod";
2
+ import type { ToolAnnotations } from "@modelcontextprotocol/sdk/types.js";
3
+ import type { PaneClient } from "@paneui/core";
4
+ /**
5
+ * A structured MCP tool result (text content + optional error flag). The
6
+ * index signature keeps it structurally assignable to the SDK's
7
+ * CallToolResult (which carries an open `[x: string]: unknown`).
8
+ */
9
+ export interface ToolResult {
10
+ content: {
11
+ type: "text";
12
+ text: string;
13
+ }[];
14
+ isError?: boolean;
15
+ [key: string]: unknown;
16
+ }
17
+ /**
18
+ * Host-supplied capabilities for the handful of tools that aren't pure
19
+ * PaneClient wrappers. The stdio server leaves this undefined and the
20
+ * handlers fall back to the CLI config store + a network skill fetch; the
21
+ * relay's HTTP MCP server injects an `env` so those tools resolve against the
22
+ * relay itself (no CLI config on disk, no self-HTTP loop for the skill).
23
+ *
24
+ * This is the single seam that keeps the TOOLS array transport-agnostic and
25
+ * reusable by BOTH servers — every other tool is already a thin PaneClient
26
+ * call and needs nothing from the host.
27
+ */
28
+ export interface ToolEnv {
29
+ /** `agent` action=whoami — describe the active identity (no secrets). */
30
+ describeConfig?: () => Record<string, unknown>;
31
+ /** `agent` action=logout — clear the locally-saved profile. */
32
+ clearProfile?: () => Record<string, unknown>;
33
+ /**
34
+ * `get_skill` — return the MCP-flavoured skill markdown + its version. The
35
+ * relay passes its in-process renderer; the stdio server fetches it over
36
+ * HTTP from the relay's /skills route.
37
+ */
38
+ getSkill?: (versionOnly: boolean) => Promise<{
39
+ markdown?: string;
40
+ version?: string;
41
+ }>;
42
+ }
43
+ /** One registered tool: name, human/LLM description, Zod input shape, handler. */
44
+ export interface ToolDef {
45
+ name: string;
46
+ description: string;
47
+ inputSchema: z.ZodRawShape;
48
+ annotations: ToolAnnotations;
49
+ handler: (client: PaneClient, args: Record<string, unknown>, env?: ToolEnv) => Promise<ToolResult>;
50
+ }
51
+ export declare const TOOLS: ToolDef[];
package/dist/tools.js CHANGED
@@ -265,7 +265,32 @@ const upgradePaneShape = {
265
265
  .int()
266
266
  .positive()
267
267
  .optional()
268
- .describe("Target version of the SAME template. Defaults to the template head's latest version."),
268
+ .describe("Target version of the SAME template. Defaults to the template head's latest version. Mutually exclusive with `html`."),
269
+ html: z
270
+ .string()
271
+ .min(1)
272
+ .optional()
273
+ .describe("INLINE EDIT: the new HTML. The relay appends a fresh template version with this HTML and re-pins the pane to it in one call — editing an INLINE pane's HTML in place (same id/URL), no separate version step needed. Any schema you don't pass below is inherited from the pane's current version, so to change only the HTML pass only `html`. Inline panes only; a named/reusable template must go through the `template` tool (action: version) + `template_version`. Mutually exclusive with `template_version`."),
274
+ template_type: z
275
+ .string()
276
+ .optional()
277
+ .describe("Type for the `html` version. Default: html-inline."),
278
+ event_schema: z
279
+ .unknown()
280
+ .optional()
281
+ .describe("New event schema for the `html` version. Omit to inherit."),
282
+ input_schema: z
283
+ .record(z.string(), z.unknown())
284
+ .optional()
285
+ .describe("New input schema for the `html` version. Omit to inherit."),
286
+ record_schema: z
287
+ .unknown()
288
+ .optional()
289
+ .describe("New record schema for the `html` version. Omit to inherit."),
290
+ template_record_schema: z
291
+ .unknown()
292
+ .optional()
293
+ .describe("New template-level record schema for the `html` version. Omit to inherit."),
269
294
  force: z
270
295
  .boolean()
271
296
  .optional()
@@ -683,8 +708,15 @@ const getSkillShape = {
683
708
  export const TOOLS = [
684
709
  {
685
710
  name: "create_pane",
686
- description: "Hand the human a rich interactive UI by URL and (optionally) get structured data back. Build the UI as inline HTML (pass `name` + `html`) OR reuse a saved template (pass `template_id`). The relay hosts it and returns a URL. ALWAYS give the returned 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, wizards. If the page captures input it emits events back to you (poll them with get_events) or mutates record collections (the record tools). Returns { pane_id, url, urls, title, expires_at }.",
711
+ description: "Hand the human a rich interactive UI by URL and (optionally) get structured data back. Build the UI as inline HTML (pass `name` + `html`) OR reuse a saved template (pass `template_id`). The relay hosts it and returns a URL. ALWAYS give the returned 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, wizards. If the page captures input it emits events back to you (poll them with get_events) or mutates record collections (the record tools). BEFORE authoring: call get_skill for the events-vs-records decision + schema grammar, and the `taste` tool (action: get) for the human's house style — both shape the HTML you write. Returns { pane_id, url, urls, title, expires_at }.",
687
712
  inputSchema: createPaneShape,
713
+ annotations: {
714
+ title: "Create Pane",
715
+ readOnlyHint: false,
716
+ destructiveHint: true,
717
+ idempotentHint: false,
718
+ openWorldHint: true,
719
+ },
688
720
  handler: async (client, args) => {
689
721
  try {
690
722
  const hasTemplateId = str(args, "template_id") !== undefined;
@@ -756,6 +788,11 @@ export const TOOLS = [
756
788
  name: "get_pane_state",
757
789
  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.",
758
790
  inputSchema: getPaneStateShape,
791
+ annotations: {
792
+ title: "Get Pane State",
793
+ readOnlyHint: true,
794
+ openWorldHint: false,
795
+ },
759
796
  handler: async (client, args) => {
760
797
  try {
761
798
  return jsonResult(await client.getPane(String(args["pane_id"])));
@@ -769,6 +806,11 @@ export const TOOLS = [
769
806
  name: "get_events",
770
807
  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 }.",
771
808
  inputSchema: getEventsShape,
809
+ annotations: {
810
+ title: "Get Events",
811
+ readOnlyHint: true,
812
+ openWorldHint: false,
813
+ },
772
814
  handler: async (client, args) => {
773
815
  try {
774
816
  const page = await client.getEvents(String(args["pane_id"]), {
@@ -786,6 +828,13 @@ export const TOOLS = [
786
828
  name: "send_to_pane",
787
829
  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 }.",
788
830
  inputSchema: sendToPaneShape,
831
+ annotations: {
832
+ title: "Send to Pane",
833
+ readOnlyHint: false,
834
+ destructiveHint: true,
835
+ idempotentHint: false,
836
+ openWorldHint: true,
837
+ },
789
838
  handler: async (client, args) => {
790
839
  try {
791
840
  const res = await client.sendEvent(String(args["pane_id"]), {
@@ -804,6 +853,13 @@ export const TOOLS = [
804
853
  name: "update_pane",
805
854
  description: "Edit instance-level fields on a LIVE pane in place (PATCH) without minting a new one — the pane keeps its id, URL, event log, and template pin. Settable: ttl_seconds OR expires_at (mutually exclusive), title, preamble, input_data (replaced wholesale + revalidated), metadata, tags, icon_emoji / icon_attachment_id (or clear_* to drop the override). Pass at least one field. Returns the full new pane state + an updated_fields array. To swap the HTML/schemas, use upgrade_pane instead.",
806
855
  inputSchema: updatePaneShape,
856
+ annotations: {
857
+ title: "Update Pane",
858
+ readOnlyHint: false,
859
+ destructiveHint: true,
860
+ idempotentHint: true,
861
+ openWorldHint: false,
862
+ },
807
863
  handler: async (client, args) => {
808
864
  try {
809
865
  const body = {};
@@ -853,13 +909,36 @@ export const TOOLS = [
853
909
  },
854
910
  {
855
911
  name: "upgrade_pane",
856
- description: "Re-pin a LIVE pane to another version of its SAME template (POST /upgrade) — swap the HTML (design) and event/input/record schemas in place. The human keeps the same URL; no new pane is created. Use after appending a new template version with the `template` tool (action: version). By default a strict schema-compat gate refuses an upgrade that would narrow the schema (returns schema_incompatible_upgrade + details.breaks); pass force:true to apply anyway. Returns { pane_id, template_version, upgraded, breaks, compat }.",
912
+ description: "Re-pin a LIVE pane to swap its HTML (design) + event/input/record schemas in place same URL, no new pane. Two ways: (1) pass `html` to EDIT AN INLINE PANE'S HTML in one call — the relay appends a fresh version with that HTML and re-pins (schemas you omit are inherited from the current version, so to change only the HTML pass only `html`); inline panes only. (2) pass `template_version` to re-pin to a version you already appended with the `template` tool (action: version) — for named/reusable templates. By default a strict schema-compat gate refuses an upgrade that would narrow the schema (returns schema_incompatible_upgrade + details.breaks); pass force:true to apply anyway. Returns { pane_id, template_version, upgraded, breaks, compat }.",
857
913
  inputSchema: upgradePaneShape,
914
+ annotations: {
915
+ title: "Upgrade Pane",
916
+ readOnlyHint: false,
917
+ destructiveHint: true,
918
+ idempotentHint: false,
919
+ openWorldHint: false,
920
+ },
858
921
  handler: async (client, args) => {
859
922
  try {
860
923
  const opts = {};
861
924
  if (args["template_version"] !== undefined)
862
925
  opts.template_version = args["template_version"];
926
+ if (args["html"] !== undefined) {
927
+ const tpl = {
928
+ source: String(args["html"]),
929
+ };
930
+ if (args["template_type"] !== undefined)
931
+ tpl.type = String(args["template_type"]);
932
+ if (args["event_schema"] !== undefined)
933
+ tpl.event_schema = args["event_schema"];
934
+ if (args["input_schema"] !== undefined)
935
+ tpl.input_schema = args["input_schema"];
936
+ if (args["record_schema"] !== undefined)
937
+ tpl.record_schema = args["record_schema"];
938
+ if (args["template_record_schema"] !== undefined)
939
+ tpl.template_record_schema = args["template_record_schema"];
940
+ opts.template = tpl;
941
+ }
863
942
  if (args["force"])
864
943
  opts.compat = "force";
865
944
  return jsonResult(await client.upgradePane(String(args["pane_id"]), opts));
@@ -873,6 +952,11 @@ export const TOOLS = [
873
952
  name: "list_panes",
874
953
  description: "Enumerate YOUR agent's panes (newest first). Use it to find a pane_id you lost, audit what's open, or get a cursor for pagination. No secrets in the response (participant tokens are unrecoverable — mint a fresh URL with the participant tool). Filter by status (open|closed|all) or template_id. Returns { items, next_cursor }.",
875
954
  inputSchema: listPanesShape,
955
+ annotations: {
956
+ title: "List Panes",
957
+ readOnlyHint: true,
958
+ openWorldHint: false,
959
+ },
876
960
  handler: async (client, args) => {
877
961
  try {
878
962
  const opts = {};
@@ -895,6 +979,13 @@ export const TOOLS = [
895
979
  name: "delete_pane",
896
980
  description: "Close/delete a pane (idempotent — an already-closed pane still succeeds). The human's URL stops working. To merely edit a pane keep it alive with update_pane; to recover a soft-deleted pane use the trash tool (action: restore).",
897
981
  inputSchema: deletePaneShape,
982
+ annotations: {
983
+ title: "Delete Pane",
984
+ readOnlyHint: false,
985
+ destructiveHint: true,
986
+ idempotentHint: true,
987
+ openWorldHint: false,
988
+ },
898
989
  handler: async (client, args) => {
899
990
  try {
900
991
  await client.deletePane(String(args["pane_id"]));
@@ -910,6 +1001,11 @@ export const TOOLS = [
910
1001
  name: "list_records",
911
1002
  description: "List rows in a pane's mutable record collection (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. This also doubles as the POLL/watch for records (no streaming in MCP): pass the prior next_since to fetch only newer/changed rows. include_tombstones:true surfaces deletions. Returns { records, next_since, has_more }.",
912
1003
  inputSchema: listRecordsShape,
1004
+ annotations: {
1005
+ title: "List Records",
1006
+ readOnlyHint: true,
1007
+ openWorldHint: false,
1008
+ },
913
1009
  handler: async (client, args) => {
914
1010
  try {
915
1011
  const out = await client.listRecords(String(args["pane_id"]), String(args["collection"]), {
@@ -934,6 +1030,11 @@ export const TOOLS = [
934
1030
  name: "get_record",
935
1031
  description: "Fetch a single record row by its key from a pane collection (scans the collection — fine for a one-off lookup, not a hot loop). Returns { record } or an isError record_not_found.",
936
1032
  inputSchema: getRecordShape,
1033
+ annotations: {
1034
+ title: "Get Record",
1035
+ readOnlyHint: true,
1036
+ openWorldHint: false,
1037
+ },
937
1038
  handler: async (client, args) => {
938
1039
  try {
939
1040
  const row = await client.getRecord(String(args["pane_id"]), String(args["collection"]), String(args["record_key"]));
@@ -949,8 +1050,15 @@ export const TOOLS = [
949
1050
  },
950
1051
  {
951
1052
  name: "upsert_record",
952
- 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 }.",
1053
+ 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. If you're still designing the pane, call get_skill first for the records-vs-events decision and the x-pane-collections schema grammar. Returns { record, deduped }.",
953
1054
  inputSchema: upsertRecordShape,
1055
+ annotations: {
1056
+ title: "Upsert Record",
1057
+ readOnlyHint: false,
1058
+ destructiveHint: true,
1059
+ idempotentHint: true,
1060
+ openWorldHint: false,
1061
+ },
954
1062
  handler: async (client, args) => {
955
1063
  try {
956
1064
  const body = {
@@ -969,6 +1077,13 @@ export const TOOLS = [
969
1077
  name: "update_record",
970
1078
  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 }.",
971
1079
  inputSchema: updateRecordShape,
1080
+ annotations: {
1081
+ title: "Update Record",
1082
+ readOnlyHint: false,
1083
+ destructiveHint: true,
1084
+ idempotentHint: true,
1085
+ openWorldHint: false,
1086
+ },
972
1087
  handler: async (client, args) => {
973
1088
  try {
974
1089
  const body = {
@@ -987,6 +1102,13 @@ export const TOOLS = [
987
1102
  name: "delete_record",
988
1103
  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). Pass if_match for an optimistic-locked delete. Returns { deleted: true }.",
989
1104
  inputSchema: deleteRecordShape,
1105
+ annotations: {
1106
+ title: "Delete Record",
1107
+ readOnlyHint: false,
1108
+ destructiveHint: true,
1109
+ idempotentHint: true,
1110
+ openWorldHint: false,
1111
+ },
990
1112
  handler: async (client, args) => {
991
1113
  try {
992
1114
  await client.deleteRecord(String(args["pane_id"]), String(args["collection"]), String(args["record_key"]), args["if_match"] !== undefined
@@ -1003,6 +1125,13 @@ export const TOOLS = [
1003
1125
  name: "delete_record_collection",
1004
1126
  description: "Drop a WHOLE per-pane record collection at once: every row plus the collection row itself. Use this to reset or remove a collection (todo list, comment thread, board) rather than deleting rows one by one with delete_record. Owner-only and destructive, so it requires confirm:true. Collection names are immutable, so to rename a collection drop the old one and write under the new name. Returns { deleted: true, collection }.",
1005
1127
  inputSchema: deleteRecordCollectionShape,
1128
+ annotations: {
1129
+ title: "Delete Record Collection",
1130
+ readOnlyHint: false,
1131
+ destructiveHint: true,
1132
+ idempotentHint: true,
1133
+ openWorldHint: false,
1134
+ },
1006
1135
  handler: async (client, args) => {
1007
1136
  try {
1008
1137
  if (args["confirm"] !== true) {
@@ -1021,6 +1150,17 @@ export const TOOLS = [
1021
1150
  name: "template",
1022
1151
  description: "Manage reusable, versioned UI templates (author once, instance many times via create_pane's template_id). ONE tool with an `action` enum: create | version | update | search | list | show | get_version | delete | publish | unpublish | search_public | set_icon. Required fields per action are documented on the `action` parameter. A template is HTML + an event schema (+ optional input/record/template-record schemas); a pane is one use of one version of it.",
1023
1152
  inputSchema: templateShape,
1153
+ // Consolidated action-enum tool: read sub-actions (search/list/show/
1154
+ // get_version/search_public) coexist with mutating ones (create/version/
1155
+ // update/delete/publish/...). The hint reflects the most-privileged action
1156
+ // (delete is destructive), so readOnlyHint:false + destructiveHint:true.
1157
+ annotations: {
1158
+ title: "Manage Templates",
1159
+ readOnlyHint: false,
1160
+ destructiveHint: true,
1161
+ idempotentHint: false,
1162
+ openWorldHint: false,
1163
+ },
1024
1164
  handler: async (client, args) => {
1025
1165
  const action = String(args["action"]);
1026
1166
  try {
@@ -1163,6 +1303,15 @@ export const TOOLS = [
1163
1303
  name: "template_records",
1164
1304
  description: "CRUD for TEMPLATE-level record collections — owner-curated content anchored to a template head and visible to every pane derived from any of its versions (vs per-pane records, which are the discrete record tools). ONE tool with an `action` enum: list | get | upsert | update | delete | delete_collection. The template version must declare the collection via template_record_schema (set it with the `template` tool first).",
1165
1305
  inputSchema: templateRecordsShape,
1306
+ // Consolidated tool: read actions (list/get) + mutating ones (upsert/
1307
+ // update/delete/delete_collection). Hint reflects the destructive action.
1308
+ annotations: {
1309
+ title: "Manage Template Records",
1310
+ readOnlyHint: false,
1311
+ destructiveHint: true,
1312
+ idempotentHint: false,
1313
+ openWorldHint: false,
1314
+ },
1166
1315
  handler: async (client, args) => {
1167
1316
  const action = String(args["action"]);
1168
1317
  const templateId = String(args["template_id"]);
@@ -1241,6 +1390,15 @@ export const TOOLS = [
1241
1390
  name: "participant",
1242
1391
  description: "Manage a pane's participant URLs (recovery + leak-containment). ONE tool with an `action` enum: list | new | revoke. Use `new` when you lost the original URL (the plaintext token is returned ONCE — save it). Token URLs are stored hashed and cannot be recovered.",
1243
1392
  inputSchema: participantShape,
1393
+ // Consolidated tool: read action (list) + mutating ones (new mints a URL,
1394
+ // revoke invalidates one). Hint reflects the destructive action.
1395
+ annotations: {
1396
+ title: "Manage Participants",
1397
+ readOnlyHint: false,
1398
+ destructiveHint: true,
1399
+ idempotentHint: false,
1400
+ openWorldHint: false,
1401
+ },
1244
1402
  handler: async (client, args) => {
1245
1403
  const action = String(args["action"]);
1246
1404
  const paneId = String(args["pane_id"]);
@@ -1272,6 +1430,16 @@ export const TOOLS = [
1272
1430
  name: "share",
1273
1431
  description: "Identity sharing on a pane (layered on top of participant tokens). ONE tool with an `action` enum: list (access_mode + grants) | invite (a human by email, role participant|viewer) | set_access (the /p access mode: invite_only|link|public) | revoke (one grant by id). Token (/s/<token>) links are independent of access_mode and keep working.",
1274
1432
  inputSchema: shareShape,
1433
+ // Consolidated tool: read action (list) + mutating/side-effecting ones
1434
+ // (invite emails a human, set_access, revoke). openWorld:true because
1435
+ // invite delivers a message to an external recipient.
1436
+ annotations: {
1437
+ title: "Manage Pane Sharing",
1438
+ readOnlyHint: false,
1439
+ destructiveHint: true,
1440
+ idempotentHint: false,
1441
+ openWorldHint: true,
1442
+ },
1275
1443
  handler: async (client, args) => {
1276
1444
  const action = String(args["action"]);
1277
1445
  const paneId = String(args["pane_id"]);
@@ -1315,6 +1483,17 @@ export const TOOLS = [
1315
1483
  name: "attachments",
1316
1484
  description: "Binary attachments (images, PDFs, audio, video) referenced from event payloads / input_data via `format: pane-attachment-id`. ONE tool with an `action` enum: upload | download | show | list | delete | mint_token | revoke_token | list_tokens. upload reads an ABSOLUTE file_path; download writes to an ABSOLUTE out_path (or returns base64). Scope an upload to agent (default, reusable), pane, or template. mint_token returns a /b/<token> capability URL (ONCE) a browser can GET without your API key.",
1317
1485
  inputSchema: attachmentsShape,
1486
+ // Consolidated tool: read actions (download/show/list/list_tokens) +
1487
+ // mutating ones (upload/delete/mint_token/revoke_token). openWorld:true
1488
+ // because upload pushes bytes into external relay storage + mint_token
1489
+ // produces a publicly-fetchable capability URL.
1490
+ annotations: {
1491
+ title: "Manage Attachments",
1492
+ readOnlyHint: false,
1493
+ destructiveHint: true,
1494
+ idempotentHint: false,
1495
+ openWorldHint: true,
1496
+ },
1318
1497
  handler: async (client, args) => {
1319
1498
  const action = String(args["action"]);
1320
1499
  try {
@@ -1414,6 +1593,15 @@ export const TOOLS = [
1414
1593
  name: "taste",
1415
1594
  description: "Read / write / clear the agent's freeform UI taste notes (a small markdown document of presentation preferences learned from human feedback — 'denser layout', 'no rounded corners'). ONE tool with an `action` enum: get | set | clear. Call `get` BEFORE generating a pane so prior feedback shapes the output; `set` does a whole-document replace (not append). Keep entries about UI/presentation only.",
1416
1595
  inputSchema: tasteShape,
1596
+ // Consolidated tool: read action (get) + mutating ones (set replaces the
1597
+ // doc, clear deletes it). Hint reflects the destructive action.
1598
+ annotations: {
1599
+ title: "Manage UI Taste Notes",
1600
+ readOnlyHint: false,
1601
+ destructiveHint: true,
1602
+ idempotentHint: false,
1603
+ openWorldHint: false,
1604
+ },
1417
1605
  handler: async (client, args) => {
1418
1606
  const action = String(args["action"]);
1419
1607
  try {
@@ -1442,6 +1630,16 @@ export const TOOLS = [
1442
1630
  name: "key",
1443
1631
  description: "Inspect or revoke the calling agent's API key. ONE tool with an `action` enum: list (key info — agent_id, key_prefix, timestamps) | revoke (self-destruct the agent's OWN key; it stops working immediately and is irreversible — pass confirm:true). The relay scopes keys to the caller, so both act only on your own key.",
1444
1632
  inputSchema: keyShape,
1633
+ // Consolidated tool: read action (list) + a mutating one (revoke
1634
+ // self-destructs the agent's own key). Hint reflects the destructive
1635
+ // action.
1636
+ annotations: {
1637
+ title: "Manage API Key",
1638
+ readOnlyHint: false,
1639
+ destructiveHint: true,
1640
+ idempotentHint: false,
1641
+ openWorldHint: false,
1642
+ },
1445
1643
  handler: async (client, args) => {
1446
1644
  const action = String(args["action"]);
1447
1645
  try {
@@ -1469,6 +1667,16 @@ export const TOOLS = [
1469
1667
  name: "trash",
1470
1668
  description: "Manage soft-deleted panes + templates. ONE tool with an `action` enum: list | restore (pane id) | restore_template (template id|slug) | purge (pane id) | purge_template (template id|slug). purge bypasses the retention window and is permanent. Soft-deleted rows live in trash until the sweeper reclaims them.",
1471
1669
  inputSchema: trashShape,
1670
+ // Consolidated tool: read action (list) + mutating ones (restore/purge/
1671
+ // restore_template/purge_template; purge is permanent). Hint reflects the
1672
+ // destructive action.
1673
+ annotations: {
1674
+ title: "Manage Trash",
1675
+ readOnlyHint: false,
1676
+ destructiveHint: true,
1677
+ idempotentHint: false,
1678
+ openWorldHint: false,
1679
+ },
1472
1680
  handler: async (client, args) => {
1473
1681
  const action = String(args["action"]);
1474
1682
  try {
@@ -1508,6 +1716,15 @@ export const TOOLS = [
1508
1716
  name: "feedback",
1509
1717
  description: "Send or list feedback to the relay operator. ONE tool with an `action` enum: create (a bug|feature|note with a message, optional pane_id) | list (the agent's own submissions, newest first, paginated by before).",
1510
1718
  inputSchema: feedbackShape,
1719
+ // Consolidated tool: read action (list) + a side-effecting one (create
1720
+ // submits feedback to the relay operator). Hint reflects the write action.
1721
+ annotations: {
1722
+ title: "Manage Feedback",
1723
+ readOnlyHint: false,
1724
+ destructiveHint: true,
1725
+ idempotentHint: false,
1726
+ openWorldHint: false,
1727
+ },
1511
1728
  handler: async (client, args) => {
1512
1729
  const action = String(args["action"]);
1513
1730
  try {
@@ -1545,19 +1762,31 @@ export const TOOLS = [
1545
1762
  name: "agent",
1546
1763
  description: "Agent identity + binding. ONE tool with an `action` enum: whoami (the resolved relay URL, active profile, whether a key is configured — no network, no secrets) | claim (bind this agent to a human via a one-shot claim code from their Settings UI; one-way) | logout (clear the locally-saved key/profile; does NOT revoke it on the relay — use the `key` tool's revoke for that).",
1547
1764
  inputSchema: agentShape,
1548
- handler: async (client, args) => {
1765
+ // Consolidated tool: read action (whoami) + mutating ones (claim binds
1766
+ // this agent to a human, logout clears the local profile). Hint reflects
1767
+ // the state-changing action.
1768
+ annotations: {
1769
+ title: "Manage Agent Identity",
1770
+ readOnlyHint: false,
1771
+ destructiveHint: true,
1772
+ idempotentHint: false,
1773
+ openWorldHint: false,
1774
+ },
1775
+ handler: async (client, args, env) => {
1549
1776
  const action = String(args["action"]);
1550
1777
  try {
1551
1778
  switch (action) {
1552
1779
  case "whoami":
1553
- // No network — pure local config introspection.
1554
- return jsonResult(describeActiveConfig());
1780
+ // No network — pure local config introspection. The relay's HTTP
1781
+ // server injects describeConfig (active token's agent identity);
1782
+ // the stdio server reads the CLI config store.
1783
+ return jsonResult((env?.describeConfig ?? describeActiveConfig)());
1555
1784
  case "claim":
1556
1785
  if (str(args, "code") === undefined)
1557
1786
  return invalidArgs("claim requires `code`");
1558
1787
  return jsonResult(await client.claimAgent(String(args["code"])));
1559
1788
  case "logout":
1560
- return jsonResult(clearActiveProfile());
1789
+ return jsonResult((env?.clearProfile ?? clearActiveProfile)());
1561
1790
  default:
1562
1791
  return invalidArgs(`unknown agent action '${action}'`);
1563
1792
  }
@@ -1571,6 +1800,11 @@ export const TOOLS = [
1571
1800
  name: "run_query",
1572
1801
  description: "Run read-only SQL over YOUR scoped data (panes, records, events) — the relay scopes every row to panes you own. Use it to summarise activity, find panes/records by content, or build a report. Tables + columns and JSON projection operators are documented on the `sql` parameter. Default output is { columns, rows, truncated, scope, elapsed_ms } (format:json); csv/tsv/table render the rows as text. Capped at 10,000 rows; 10s timeout.",
1573
1802
  inputSchema: runQueryShape,
1803
+ annotations: {
1804
+ title: "Run SQL Query",
1805
+ readOnlyHint: true,
1806
+ openWorldHint: false,
1807
+ },
1574
1808
  handler: async (client, args) => {
1575
1809
  try {
1576
1810
  const result = await client.query(String(args["sql"]), str(args, "pane_id") !== undefined
@@ -1592,10 +1826,26 @@ export const TOOLS = [
1592
1826
  name: "get_skill",
1593
1827
  description: "Fetch the relay's auto-updating SKILL.md (the full Pane usage guide) — UNAUTHENTICATED, needs no API key. Call this to self-teach the Pane workflow (events vs records, schema grammars, the poll loop) before driving the other tools. Pass version_only:true to get just the relay's skill version string (to check if a cached copy is stale).",
1594
1828
  inputSchema: getSkillShape,
1595
- handler: async (_client, args) => {
1829
+ annotations: {
1830
+ title: "Get Skill Guide",
1831
+ readOnlyHint: true,
1832
+ openWorldHint: false,
1833
+ },
1834
+ handler: async (_client, args, env) => {
1596
1835
  try {
1836
+ const versionOnly = args["version_only"] === true;
1837
+ // The relay's HTTP server injects getSkill so MCP consumers receive
1838
+ // the MCP-invocation rendering of the skill (tool-call grammar, not
1839
+ // `pane ...` commands) straight from the relay image. The stdio server
1840
+ // falls back to fetching SKILL.md over HTTP from its configured relay.
1841
+ if (env?.getSkill) {
1842
+ const { markdown, version } = await env.getSkill(versionOnly);
1843
+ if (versionOnly)
1844
+ return jsonResult({ version });
1845
+ return textResult(markdown ?? "");
1846
+ }
1597
1847
  const url = resolveUrl();
1598
- if (args["version_only"]) {
1848
+ if (versionOnly) {
1599
1849
  const { version } = await fetchSkill(url, { version: true });
1600
1850
  return jsonResult({ version });
1601
1851
  }
@@ -0,0 +1 @@
1
+ export declare const VERSION = "0.0.25";
package/dist/version.js CHANGED
@@ -1,3 +1,3 @@
1
1
  // Single source of the package version, reported in the MCP server's
2
2
  // serverInfo. Kept in sync with package.json by the release tooling.
3
- export const VERSION = "0.0.23";
3
+ export const VERSION = "0.0.25";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paneui/mcp",
3
- "version": "0.0.23",
3
+ "version": "0.0.25",
4
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
5
  "license": "MIT",
6
6
  "type": "module",
@@ -30,6 +30,12 @@
30
30
  "bin": {
31
31
  "pane-mcp": "dist/index.js"
32
32
  },
33
+ "exports": {
34
+ "./tools": "./dist/tools.js",
35
+ "./guide": "./dist/guide.js",
36
+ "./capabilities": "./dist/capabilities.js",
37
+ "./server": "./dist/server.js"
38
+ },
33
39
  "files": [
34
40
  "dist",
35
41
  "server.json",
@@ -44,7 +50,7 @@
44
50
  },
45
51
  "dependencies": {
46
52
  "@modelcontextprotocol/sdk": "^1.20.0",
47
- "@paneui/core": "^0.0.23",
53
+ "@paneui/core": "^0.0.25",
48
54
  "zod": "^4.4.3"
49
55
  },
50
56
  "devDependencies": {
package/server.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "name": "io.github.aerolalit/pane",
4
4
  "title": "Pane",
5
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.23",
6
+ "version": "0.0.25",
7
7
  "repository": {
8
8
  "url": "https://github.com/aerolalit/paneui",
9
9
  "source": "github"
@@ -13,7 +13,7 @@
13
13
  "registryType": "npm",
14
14
  "registryBaseUrl": "https://registry.npmjs.org",
15
15
  "identifier": "@paneui/mcp",
16
- "version": "0.0.23",
16
+ "version": "0.0.25",
17
17
  "transport": {
18
18
  "type": "stdio"
19
19
  },