@paneui/mcp 0.0.22 → 0.0.24

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/README.md CHANGED
@@ -117,6 +117,7 @@ To keep the tool list compact (a flat 50+ tools would bloat client context and d
117
117
  | `upsert_record` | Create/return a record row (dedups on `record_key`). |
118
118
  | `update_record` | Update a record row (optional `if_match` optimistic lock). |
119
119
  | `delete_record` | Soft-delete a record row (the page sees it live). |
120
+ | `delete_record_collection` | Drop a whole record collection (all rows + the collection row). Destructive, owner-only, requires `confirm: true`. |
120
121
 
121
122
  ### Consolidated tools (one tool, required `action`)
122
123
 
@@ -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
@@ -8,8 +8,8 @@
8
8
  // Surface design (full parity with the `pane` CLI):
9
9
  // - Hot-path nouns are DISCRETE tools with sharp descriptions: create_pane,
10
10
  // get_pane_state, get_events, send_to_pane, update_pane, delete_pane,
11
- // upgrade_pane, list_panes, and the four record CRUD tools (list_records,
12
- // upsert_record, update_record, delete_record).
11
+ // upgrade_pane, list_panes, and the record tools (list_records, get_record,
12
+ // upsert_record, update_record, delete_record, delete_record_collection).
13
13
  // - Multi-verb MANAGEMENT nouns each collapse into ONE tool with a required
14
14
  // `action` enum and per-action fields: records_admin (template/per-pane
15
15
  // collection admin lives under the discrete record tools + this one for the
@@ -359,6 +359,16 @@ const deleteRecordShape = {
359
359
  .optional()
360
360
  .describe("Optional optimistic-lock version."),
361
361
  };
362
+ const deleteRecordCollectionShape = {
363
+ pane_id: z.string().min(1).describe("The pane id."),
364
+ collection: z
365
+ .string()
366
+ .min(1)
367
+ .describe("The record collection to drop in its entirety."),
368
+ confirm: z
369
+ .literal(true)
370
+ .describe("Required (true) to drop the whole collection. This removes every row plus the collection row itself and cannot be undone."),
371
+ };
362
372
  // ===========================================================================
363
373
  // Consolidated management tools
364
374
  // ===========================================================================
@@ -673,8 +683,15 @@ const getSkillShape = {
673
683
  export const TOOLS = [
674
684
  {
675
685
  name: "create_pane",
676
- 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 }.",
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). 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 }.",
677
687
  inputSchema: createPaneShape,
688
+ annotations: {
689
+ title: "Create Pane",
690
+ readOnlyHint: false,
691
+ destructiveHint: true,
692
+ idempotentHint: false,
693
+ openWorldHint: true,
694
+ },
678
695
  handler: async (client, args) => {
679
696
  try {
680
697
  const hasTemplateId = str(args, "template_id") !== undefined;
@@ -746,6 +763,11 @@ export const TOOLS = [
746
763
  name: "get_pane_state",
747
764
  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.",
748
765
  inputSchema: getPaneStateShape,
766
+ annotations: {
767
+ title: "Get Pane State",
768
+ readOnlyHint: true,
769
+ openWorldHint: false,
770
+ },
749
771
  handler: async (client, args) => {
750
772
  try {
751
773
  return jsonResult(await client.getPane(String(args["pane_id"])));
@@ -759,6 +781,11 @@ export const TOOLS = [
759
781
  name: "get_events",
760
782
  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 }.",
761
783
  inputSchema: getEventsShape,
784
+ annotations: {
785
+ title: "Get Events",
786
+ readOnlyHint: true,
787
+ openWorldHint: false,
788
+ },
762
789
  handler: async (client, args) => {
763
790
  try {
764
791
  const page = await client.getEvents(String(args["pane_id"]), {
@@ -776,6 +803,13 @@ export const TOOLS = [
776
803
  name: "send_to_pane",
777
804
  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 }.",
778
805
  inputSchema: sendToPaneShape,
806
+ annotations: {
807
+ title: "Send to Pane",
808
+ readOnlyHint: false,
809
+ destructiveHint: true,
810
+ idempotentHint: false,
811
+ openWorldHint: true,
812
+ },
779
813
  handler: async (client, args) => {
780
814
  try {
781
815
  const res = await client.sendEvent(String(args["pane_id"]), {
@@ -794,6 +828,13 @@ export const TOOLS = [
794
828
  name: "update_pane",
795
829
  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.",
796
830
  inputSchema: updatePaneShape,
831
+ annotations: {
832
+ title: "Update Pane",
833
+ readOnlyHint: false,
834
+ destructiveHint: true,
835
+ idempotentHint: true,
836
+ openWorldHint: false,
837
+ },
797
838
  handler: async (client, args) => {
798
839
  try {
799
840
  const body = {};
@@ -845,6 +886,13 @@ export const TOOLS = [
845
886
  name: "upgrade_pane",
846
887
  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 }.",
847
888
  inputSchema: upgradePaneShape,
889
+ annotations: {
890
+ title: "Upgrade Pane",
891
+ readOnlyHint: false,
892
+ destructiveHint: true,
893
+ idempotentHint: false,
894
+ openWorldHint: false,
895
+ },
848
896
  handler: async (client, args) => {
849
897
  try {
850
898
  const opts = {};
@@ -863,6 +911,11 @@ export const TOOLS = [
863
911
  name: "list_panes",
864
912
  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 }.",
865
913
  inputSchema: listPanesShape,
914
+ annotations: {
915
+ title: "List Panes",
916
+ readOnlyHint: true,
917
+ openWorldHint: false,
918
+ },
866
919
  handler: async (client, args) => {
867
920
  try {
868
921
  const opts = {};
@@ -885,6 +938,13 @@ export const TOOLS = [
885
938
  name: "delete_pane",
886
939
  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).",
887
940
  inputSchema: deletePaneShape,
941
+ annotations: {
942
+ title: "Delete Pane",
943
+ readOnlyHint: false,
944
+ destructiveHint: true,
945
+ idempotentHint: true,
946
+ openWorldHint: false,
947
+ },
888
948
  handler: async (client, args) => {
889
949
  try {
890
950
  await client.deletePane(String(args["pane_id"]));
@@ -900,6 +960,11 @@ export const TOOLS = [
900
960
  name: "list_records",
901
961
  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 }.",
902
962
  inputSchema: listRecordsShape,
963
+ annotations: {
964
+ title: "List Records",
965
+ readOnlyHint: true,
966
+ openWorldHint: false,
967
+ },
903
968
  handler: async (client, args) => {
904
969
  try {
905
970
  const out = await client.listRecords(String(args["pane_id"]), String(args["collection"]), {
@@ -924,6 +989,11 @@ export const TOOLS = [
924
989
  name: "get_record",
925
990
  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.",
926
991
  inputSchema: getRecordShape,
992
+ annotations: {
993
+ title: "Get Record",
994
+ readOnlyHint: true,
995
+ openWorldHint: false,
996
+ },
927
997
  handler: async (client, args) => {
928
998
  try {
929
999
  const row = await client.getRecord(String(args["pane_id"]), String(args["collection"]), String(args["record_key"]));
@@ -939,8 +1009,15 @@ export const TOOLS = [
939
1009
  },
940
1010
  {
941
1011
  name: "upsert_record",
942
- 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 }.",
1012
+ 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 }.",
943
1013
  inputSchema: upsertRecordShape,
1014
+ annotations: {
1015
+ title: "Upsert Record",
1016
+ readOnlyHint: false,
1017
+ destructiveHint: true,
1018
+ idempotentHint: true,
1019
+ openWorldHint: false,
1020
+ },
944
1021
  handler: async (client, args) => {
945
1022
  try {
946
1023
  const body = {
@@ -959,6 +1036,13 @@ export const TOOLS = [
959
1036
  name: "update_record",
960
1037
  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 }.",
961
1038
  inputSchema: updateRecordShape,
1039
+ annotations: {
1040
+ title: "Update Record",
1041
+ readOnlyHint: false,
1042
+ destructiveHint: true,
1043
+ idempotentHint: true,
1044
+ openWorldHint: false,
1045
+ },
962
1046
  handler: async (client, args) => {
963
1047
  try {
964
1048
  const body = {
@@ -977,6 +1061,13 @@ export const TOOLS = [
977
1061
  name: "delete_record",
978
1062
  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 }.",
979
1063
  inputSchema: deleteRecordShape,
1064
+ annotations: {
1065
+ title: "Delete Record",
1066
+ readOnlyHint: false,
1067
+ destructiveHint: true,
1068
+ idempotentHint: true,
1069
+ openWorldHint: false,
1070
+ },
980
1071
  handler: async (client, args) => {
981
1072
  try {
982
1073
  await client.deleteRecord(String(args["pane_id"]), String(args["collection"]), String(args["record_key"]), args["if_match"] !== undefined
@@ -989,11 +1080,46 @@ export const TOOLS = [
989
1080
  }
990
1081
  },
991
1082
  },
1083
+ {
1084
+ name: "delete_record_collection",
1085
+ 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 }.",
1086
+ inputSchema: deleteRecordCollectionShape,
1087
+ annotations: {
1088
+ title: "Delete Record Collection",
1089
+ readOnlyHint: false,
1090
+ destructiveHint: true,
1091
+ idempotentHint: true,
1092
+ openWorldHint: false,
1093
+ },
1094
+ handler: async (client, args) => {
1095
+ try {
1096
+ if (args["confirm"] !== true) {
1097
+ return invalidArgs("delete_record_collection drops the whole collection. Pass confirm:true to proceed.");
1098
+ }
1099
+ await client.deleteRecordCollection(String(args["pane_id"]), String(args["collection"]));
1100
+ return jsonResult({ deleted: true, collection: args["collection"] });
1101
+ }
1102
+ catch (e) {
1103
+ return errorResult(e);
1104
+ }
1105
+ },
1106
+ },
992
1107
  // ----- consolidated management tools --------------------------------------
993
1108
  {
994
1109
  name: "template",
995
1110
  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.",
996
1111
  inputSchema: templateShape,
1112
+ // Consolidated action-enum tool: read sub-actions (search/list/show/
1113
+ // get_version/search_public) coexist with mutating ones (create/version/
1114
+ // update/delete/publish/...). The hint reflects the most-privileged action
1115
+ // (delete is destructive), so readOnlyHint:false + destructiveHint:true.
1116
+ annotations: {
1117
+ title: "Manage Templates",
1118
+ readOnlyHint: false,
1119
+ destructiveHint: true,
1120
+ idempotentHint: false,
1121
+ openWorldHint: false,
1122
+ },
997
1123
  handler: async (client, args) => {
998
1124
  const action = String(args["action"]);
999
1125
  try {
@@ -1136,6 +1262,15 @@ export const TOOLS = [
1136
1262
  name: "template_records",
1137
1263
  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).",
1138
1264
  inputSchema: templateRecordsShape,
1265
+ // Consolidated tool: read actions (list/get) + mutating ones (upsert/
1266
+ // update/delete/delete_collection). Hint reflects the destructive action.
1267
+ annotations: {
1268
+ title: "Manage Template Records",
1269
+ readOnlyHint: false,
1270
+ destructiveHint: true,
1271
+ idempotentHint: false,
1272
+ openWorldHint: false,
1273
+ },
1139
1274
  handler: async (client, args) => {
1140
1275
  const action = String(args["action"]);
1141
1276
  const templateId = String(args["template_id"]);
@@ -1214,6 +1349,15 @@ export const TOOLS = [
1214
1349
  name: "participant",
1215
1350
  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.",
1216
1351
  inputSchema: participantShape,
1352
+ // Consolidated tool: read action (list) + mutating ones (new mints a URL,
1353
+ // revoke invalidates one). Hint reflects the destructive action.
1354
+ annotations: {
1355
+ title: "Manage Participants",
1356
+ readOnlyHint: false,
1357
+ destructiveHint: true,
1358
+ idempotentHint: false,
1359
+ openWorldHint: false,
1360
+ },
1217
1361
  handler: async (client, args) => {
1218
1362
  const action = String(args["action"]);
1219
1363
  const paneId = String(args["pane_id"]);
@@ -1245,6 +1389,16 @@ export const TOOLS = [
1245
1389
  name: "share",
1246
1390
  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.",
1247
1391
  inputSchema: shareShape,
1392
+ // Consolidated tool: read action (list) + mutating/side-effecting ones
1393
+ // (invite emails a human, set_access, revoke). openWorld:true because
1394
+ // invite delivers a message to an external recipient.
1395
+ annotations: {
1396
+ title: "Manage Pane Sharing",
1397
+ readOnlyHint: false,
1398
+ destructiveHint: true,
1399
+ idempotentHint: false,
1400
+ openWorldHint: true,
1401
+ },
1248
1402
  handler: async (client, args) => {
1249
1403
  const action = String(args["action"]);
1250
1404
  const paneId = String(args["pane_id"]);
@@ -1288,6 +1442,17 @@ export const TOOLS = [
1288
1442
  name: "attachments",
1289
1443
  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.",
1290
1444
  inputSchema: attachmentsShape,
1445
+ // Consolidated tool: read actions (download/show/list/list_tokens) +
1446
+ // mutating ones (upload/delete/mint_token/revoke_token). openWorld:true
1447
+ // because upload pushes bytes into external relay storage + mint_token
1448
+ // produces a publicly-fetchable capability URL.
1449
+ annotations: {
1450
+ title: "Manage Attachments",
1451
+ readOnlyHint: false,
1452
+ destructiveHint: true,
1453
+ idempotentHint: false,
1454
+ openWorldHint: true,
1455
+ },
1291
1456
  handler: async (client, args) => {
1292
1457
  const action = String(args["action"]);
1293
1458
  try {
@@ -1387,6 +1552,15 @@ export const TOOLS = [
1387
1552
  name: "taste",
1388
1553
  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.",
1389
1554
  inputSchema: tasteShape,
1555
+ // Consolidated tool: read action (get) + mutating ones (set replaces the
1556
+ // doc, clear deletes it). Hint reflects the destructive action.
1557
+ annotations: {
1558
+ title: "Manage UI Taste Notes",
1559
+ readOnlyHint: false,
1560
+ destructiveHint: true,
1561
+ idempotentHint: false,
1562
+ openWorldHint: false,
1563
+ },
1390
1564
  handler: async (client, args) => {
1391
1565
  const action = String(args["action"]);
1392
1566
  try {
@@ -1415,6 +1589,16 @@ export const TOOLS = [
1415
1589
  name: "key",
1416
1590
  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.",
1417
1591
  inputSchema: keyShape,
1592
+ // Consolidated tool: read action (list) + a mutating one (revoke
1593
+ // self-destructs the agent's own key). Hint reflects the destructive
1594
+ // action.
1595
+ annotations: {
1596
+ title: "Manage API Key",
1597
+ readOnlyHint: false,
1598
+ destructiveHint: true,
1599
+ idempotentHint: false,
1600
+ openWorldHint: false,
1601
+ },
1418
1602
  handler: async (client, args) => {
1419
1603
  const action = String(args["action"]);
1420
1604
  try {
@@ -1442,6 +1626,16 @@ export const TOOLS = [
1442
1626
  name: "trash",
1443
1627
  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.",
1444
1628
  inputSchema: trashShape,
1629
+ // Consolidated tool: read action (list) + mutating ones (restore/purge/
1630
+ // restore_template/purge_template; purge is permanent). Hint reflects the
1631
+ // destructive action.
1632
+ annotations: {
1633
+ title: "Manage Trash",
1634
+ readOnlyHint: false,
1635
+ destructiveHint: true,
1636
+ idempotentHint: false,
1637
+ openWorldHint: false,
1638
+ },
1445
1639
  handler: async (client, args) => {
1446
1640
  const action = String(args["action"]);
1447
1641
  try {
@@ -1481,6 +1675,15 @@ export const TOOLS = [
1481
1675
  name: "feedback",
1482
1676
  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).",
1483
1677
  inputSchema: feedbackShape,
1678
+ // Consolidated tool: read action (list) + a side-effecting one (create
1679
+ // submits feedback to the relay operator). Hint reflects the write action.
1680
+ annotations: {
1681
+ title: "Manage Feedback",
1682
+ readOnlyHint: false,
1683
+ destructiveHint: true,
1684
+ idempotentHint: false,
1685
+ openWorldHint: false,
1686
+ },
1484
1687
  handler: async (client, args) => {
1485
1688
  const action = String(args["action"]);
1486
1689
  try {
@@ -1518,19 +1721,31 @@ export const TOOLS = [
1518
1721
  name: "agent",
1519
1722
  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).",
1520
1723
  inputSchema: agentShape,
1521
- handler: async (client, args) => {
1724
+ // Consolidated tool: read action (whoami) + mutating ones (claim binds
1725
+ // this agent to a human, logout clears the local profile). Hint reflects
1726
+ // the state-changing action.
1727
+ annotations: {
1728
+ title: "Manage Agent Identity",
1729
+ readOnlyHint: false,
1730
+ destructiveHint: true,
1731
+ idempotentHint: false,
1732
+ openWorldHint: false,
1733
+ },
1734
+ handler: async (client, args, env) => {
1522
1735
  const action = String(args["action"]);
1523
1736
  try {
1524
1737
  switch (action) {
1525
1738
  case "whoami":
1526
- // No network — pure local config introspection.
1527
- return jsonResult(describeActiveConfig());
1739
+ // No network — pure local config introspection. The relay's HTTP
1740
+ // server injects describeConfig (active token's agent identity);
1741
+ // the stdio server reads the CLI config store.
1742
+ return jsonResult((env?.describeConfig ?? describeActiveConfig)());
1528
1743
  case "claim":
1529
1744
  if (str(args, "code") === undefined)
1530
1745
  return invalidArgs("claim requires `code`");
1531
1746
  return jsonResult(await client.claimAgent(String(args["code"])));
1532
1747
  case "logout":
1533
- return jsonResult(clearActiveProfile());
1748
+ return jsonResult((env?.clearProfile ?? clearActiveProfile)());
1534
1749
  default:
1535
1750
  return invalidArgs(`unknown agent action '${action}'`);
1536
1751
  }
@@ -1544,6 +1759,11 @@ export const TOOLS = [
1544
1759
  name: "run_query",
1545
1760
  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.",
1546
1761
  inputSchema: runQueryShape,
1762
+ annotations: {
1763
+ title: "Run SQL Query",
1764
+ readOnlyHint: true,
1765
+ openWorldHint: false,
1766
+ },
1547
1767
  handler: async (client, args) => {
1548
1768
  try {
1549
1769
  const result = await client.query(String(args["sql"]), str(args, "pane_id") !== undefined
@@ -1565,10 +1785,26 @@ export const TOOLS = [
1565
1785
  name: "get_skill",
1566
1786
  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).",
1567
1787
  inputSchema: getSkillShape,
1568
- handler: async (_client, args) => {
1788
+ annotations: {
1789
+ title: "Get Skill Guide",
1790
+ readOnlyHint: true,
1791
+ openWorldHint: false,
1792
+ },
1793
+ handler: async (_client, args, env) => {
1569
1794
  try {
1795
+ const versionOnly = args["version_only"] === true;
1796
+ // The relay's HTTP server injects getSkill so MCP consumers receive
1797
+ // the MCP-invocation rendering of the skill (tool-call grammar, not
1798
+ // `pane ...` commands) straight from the relay image. The stdio server
1799
+ // falls back to fetching SKILL.md over HTTP from its configured relay.
1800
+ if (env?.getSkill) {
1801
+ const { markdown, version } = await env.getSkill(versionOnly);
1802
+ if (versionOnly)
1803
+ return jsonResult({ version });
1804
+ return textResult(markdown ?? "");
1805
+ }
1570
1806
  const url = resolveUrl();
1571
- if (args["version_only"]) {
1807
+ if (versionOnly) {
1572
1808
  const { version } = await fetchSkill(url, { version: true });
1573
1809
  return jsonResult({ version });
1574
1810
  }
@@ -0,0 +1 @@
1
+ export declare const VERSION = "0.0.24";
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.19";
3
+ export const VERSION = "0.0.24";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paneui/mcp",
3
- "version": "0.0.22",
3
+ "version": "0.0.24",
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.22",
53
+ "@paneui/core": "^0.0.24",
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.19",
6
+ "version": "0.0.24",
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.19",
16
+ "version": "0.0.24",
17
17
  "transport": {
18
18
  "type": "stdio"
19
19
  },