plonk-mcp 0.0.1
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/dist/api.js +42 -0
- package/dist/schemas.js +49 -0
- package/dist/server.js +27 -0
- package/dist/tools/annotate.js +27 -0
- package/dist/tools/awake.js +8 -0
- package/dist/tools/layouts.js +21 -0
- package/dist/tools/screenshot.js +51 -0
- package/dist/tools/state.js +4 -0
- package/dist/tools/workspaces.js +29 -0
- package/dist/tools/zones.js +15 -0
- package/package.json +40 -0
package/dist/api.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export const BASE = "http://127.0.0.1:43917";
|
|
2
|
+
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
3
|
+
const NOT_RUNNING = "Plonk menu bar app is not running. Ask the user to launch Plonk.app (its icon should appear in the menu bar).";
|
|
4
|
+
export async function call(path, options = {}) {
|
|
5
|
+
const { method = "GET", body, timeoutMs = DEFAULT_TIMEOUT_MS } = options;
|
|
6
|
+
const timeout = AbortSignal.timeout(timeoutMs);
|
|
7
|
+
let res;
|
|
8
|
+
try {
|
|
9
|
+
res = await fetch(BASE + path, {
|
|
10
|
+
method,
|
|
11
|
+
headers: { "content-type": "application/json" },
|
|
12
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
13
|
+
signal: timeout,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
catch (err) {
|
|
17
|
+
if (timeout.aborted) {
|
|
18
|
+
return { error: `Plonk did not answer within ${timeoutMs / 1000}s. It may be waiting on a dialog.` };
|
|
19
|
+
}
|
|
20
|
+
return { error: NOT_RUNNING };
|
|
21
|
+
}
|
|
22
|
+
const text = await res.text();
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(text);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return { error: `Plonk returned ${res.status} with an unexpected body: ${text.slice(0, 200)}` };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Whether the app is answering. Several servers may run at once — one per MCP
|
|
32
|
+
* client — but all of them are useless without the app behind the port.
|
|
33
|
+
*/
|
|
34
|
+
export async function isAppReachable(timeoutMs = 2_000) {
|
|
35
|
+
return !("error" in (await call("/ping", { timeoutMs })));
|
|
36
|
+
}
|
|
37
|
+
// An `error` key means the app refused or was unreachable; flagging it stops
|
|
38
|
+
// the model from reading the failure as a successful call.
|
|
39
|
+
export const text = (obj) => ({
|
|
40
|
+
content: [{ type: "text", text: JSON.stringify(obj, null, 2) }],
|
|
41
|
+
...("error" in obj ? { isError: true } : {}),
|
|
42
|
+
});
|
package/dist/schemas.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Shared zod schemas for tool inputs.
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
export const frameSchema = z.object({
|
|
4
|
+
x: z.number().min(0).max(1),
|
|
5
|
+
y: z.number().min(0).max(1),
|
|
6
|
+
w: z.number().min(0).max(1),
|
|
7
|
+
h: z.number().min(0).max(1),
|
|
8
|
+
});
|
|
9
|
+
export const itemsSchema = z
|
|
10
|
+
.array(z.object({
|
|
11
|
+
app: z.string().describe("App name to match, e.g. 'Safari', 'Visual Studio Code'"),
|
|
12
|
+
title: z.string().optional().describe("Only windows whose title contains this substring"),
|
|
13
|
+
screen: z.number().int().optional().describe("Monitor index from get_state (0 = primary)"),
|
|
14
|
+
frame: frameSchema,
|
|
15
|
+
}))
|
|
16
|
+
.min(1);
|
|
17
|
+
export const zonesSchema = z.array(frameSchema).min(1);
|
|
18
|
+
export const workspaceItemsSchema = z
|
|
19
|
+
.array(z.object({
|
|
20
|
+
app: z.string().describe("App name as it appears in get_state, e.g. 'Safari'"),
|
|
21
|
+
bundle_id: z
|
|
22
|
+
.string()
|
|
23
|
+
.optional()
|
|
24
|
+
.describe("Bundle identifier, e.g. 'com.apple.Safari'. Required to launch an app that is not running"),
|
|
25
|
+
bundle_path: z.string().optional().describe("Path to the .app, from get_state"),
|
|
26
|
+
title: z.string().optional().describe("Prefer windows whose title contains this substring"),
|
|
27
|
+
window_index: z
|
|
28
|
+
.number()
|
|
29
|
+
.int()
|
|
30
|
+
.min(0)
|
|
31
|
+
.optional()
|
|
32
|
+
.describe("Which window of that app, when the workspace holds several of them"),
|
|
33
|
+
screen: z.number().int().optional().describe("Monitor index from get_state (0 = primary)"),
|
|
34
|
+
screen_uuid: z
|
|
35
|
+
.string()
|
|
36
|
+
.optional()
|
|
37
|
+
.describe("Display UUID from a saved workspace; survives monitors being unplugged, unlike the index"),
|
|
38
|
+
frame: frameSchema,
|
|
39
|
+
minimized: z.boolean().optional().describe("Minimize the window once it is in place"),
|
|
40
|
+
urls: z
|
|
41
|
+
.array(z.string())
|
|
42
|
+
.optional()
|
|
43
|
+
.describe("Files, folders or URLs this app opens when the workspace launches it, e.g. a project folder for an editor or a set of tabs for a browser"),
|
|
44
|
+
args: z
|
|
45
|
+
.array(z.string())
|
|
46
|
+
.optional()
|
|
47
|
+
.describe("Launch arguments. Many Mac apps ignore these; prefer 'urls'"),
|
|
48
|
+
}))
|
|
49
|
+
.min(1);
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Plonk MCP server — bridges AI agents to the Plonk menu bar app (localhost HTTP).
|
|
3
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import { BASE, isAppReachable } from "./api.js";
|
|
6
|
+
import { register as registerState } from "./tools/state.js";
|
|
7
|
+
import { register as registerLayouts } from "./tools/layouts.js";
|
|
8
|
+
import { register as registerWorkspaces } from "./tools/workspaces.js";
|
|
9
|
+
import { register as registerZones } from "./tools/zones.js";
|
|
10
|
+
import { register as registerAwake } from "./tools/awake.js";
|
|
11
|
+
import { register as registerScreenshot } from "./tools/screenshot.js";
|
|
12
|
+
import { register as registerAnnotate } from "./tools/annotate.js";
|
|
13
|
+
const server = new McpServer({ name: "plonk", version: "1.0.0" });
|
|
14
|
+
registerState(server);
|
|
15
|
+
registerWorkspaces(server);
|
|
16
|
+
registerLayouts(server);
|
|
17
|
+
registerZones(server);
|
|
18
|
+
registerAwake(server);
|
|
19
|
+
registerScreenshot(server);
|
|
20
|
+
registerAnnotate(server);
|
|
21
|
+
const transport = new StdioServerTransport();
|
|
22
|
+
await server.connect(transport);
|
|
23
|
+
// stdout carries the protocol, so this goes to stderr. Not fatal: the app may
|
|
24
|
+
// still be starting, and every tool reports the same thing on its own.
|
|
25
|
+
if (!(await isAppReachable())) {
|
|
26
|
+
console.error(`plonk-mcp: nothing is answering on ${BASE} — launch Plonk.app or its tools will fail.`);
|
|
27
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { call, text } from "../api.js";
|
|
3
|
+
const pointSchema = z.object({
|
|
4
|
+
x: z.number().min(0).max(1),
|
|
5
|
+
y: z.number().min(0).max(1),
|
|
6
|
+
});
|
|
7
|
+
export function register(server) {
|
|
8
|
+
server.tool("annotate_screenshot", "Draw on a screenshot you already took, then copy it to the clipboard and show it to the user. Call take_screenshot first and LOOK at the image: you cannot know where anything is until you have seen it. Points are fractions 0..1 of the image, origin TOP-LEFT, so a rectangle around a left sidebar that is a seventh of the width and starts under the title bar is [{x:0,y:0.05},{x:0.14,y:1}]. Rectangle and ellipse take two opposite corners, arrow takes start then tip, pen and highlight take a run of points. Returns the marked image so you can check what you drew.", {
|
|
9
|
+
path: z.string().describe("Path returned by take_screenshot"),
|
|
10
|
+
marks: z
|
|
11
|
+
.array(z.object({
|
|
12
|
+
kind: z.enum(["rectangle", "ellipse", "arrow", "pen", "highlight"]),
|
|
13
|
+
points: z.array(pointSchema).min(2),
|
|
14
|
+
color: z
|
|
15
|
+
.enum(["red", "orange", "yellow", "green", "blue", "purple", "black", "white"])
|
|
16
|
+
.optional()
|
|
17
|
+
.describe("Defaults to red"),
|
|
18
|
+
width: z
|
|
19
|
+
.number()
|
|
20
|
+
.optional()
|
|
21
|
+
.describe("Stroke width as a fraction of image width; defaults to 0.004"),
|
|
22
|
+
}))
|
|
23
|
+
.min(1),
|
|
24
|
+
output: z.string().optional().describe("Where to write it; defaults to the source name plus ' marked'"),
|
|
25
|
+
clipboard: z.boolean().optional().describe("Copy the result to the clipboard (default true)"),
|
|
26
|
+
}, async (args) => text(await call("/shot/annotate", { method: "POST", body: args })));
|
|
27
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { call, text } from "../api.js";
|
|
3
|
+
export function register(server) {
|
|
4
|
+
server.tool("set_awake", "Turn keep-awake on or off. Optional 'minutes' limits the session (it ends automatically). Behavior also follows the user's settings: keep-awake may pause on battery or engage automatically while charging; the returned 'status' explains the current state. The menu bar icon glows while active.", {
|
|
5
|
+
on: z.boolean(),
|
|
6
|
+
minutes: z.number().int().min(1).optional().describe("Auto-off after this many minutes"),
|
|
7
|
+
}, async ({ on, minutes }) => text(await call("/awake", { method: "POST", body: { on, minutes } })));
|
|
8
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { call, text } from "../api.js";
|
|
3
|
+
import { itemsSchema } from "../schemas.js";
|
|
4
|
+
export function register(server) {
|
|
5
|
+
server.tool("apply_layout", "Move and resize windows to build a layout. Each item places one window: 'app' is the app name (fuzzy matched), optional 'title' filters windows of that app by title substring, optional 'screen' is the monitor index from get_state (each monitor can get its own layout — just send items with different 'screen' values; defaults to the screen the window is currently on), 'frame' is {x,y,w,h} as fractions 0..1 of that screen's visible area with origin at TOP-LEFT (left half = {x:0,y:0,w:0.5,h:1}; bottom-right quarter = {x:0.5,y:0.5,w:0.5,h:0.5}; centered 60% = {x:0.2,y:0.15,w:0.6,h:0.7}). Windows are unminimized if needed. Returns per-item success/errors.", { items: itemsSchema }, async ({ items }) => text(await call("/layout", { method: "POST", body: { items } })));
|
|
6
|
+
// Saved layouts became workspaces, which also know how to launch their apps.
|
|
7
|
+
// These two stay so older clients keep working; prefer save_workspace and
|
|
8
|
+
// launch_workspace, which can carry an app's bundle id and what it opens.
|
|
9
|
+
server.tool("save_layout", "Deprecated alias of save_workspace. Saves the named arrangement as a workspace; omit 'items' to snapshot what is on screen right now.", {
|
|
10
|
+
name: z.string().describe("Workspace name, e.g. 'work', 'focus'"),
|
|
11
|
+
items: itemsSchema.optional(),
|
|
12
|
+
}, async ({ name, items }) => text(await call("/workspaces/save", { method: "POST", body: { name, items } })));
|
|
13
|
+
server.tool("apply_saved_layout", "Deprecated alias of launch_workspace. Launches the saved workspace of that name, opening any app that is not running.", { name: z.string() }, async ({ name }) => text(await call("/workspaces/launch", { method: "POST", body: { name }, timeoutMs: 90_000 })));
|
|
14
|
+
server.tool("snap_window", "Drop one window into a numbered zone of the snap-zone set assigned to that monitor. The numbers are the ones Plonk draws on the zones while a window is dragged, so 'the middle zone' of a three-zone set is 2. Zone sets and their per-monitor assignment are in get_state; use apply_layout instead when the user describes a size rather than a zone.", {
|
|
15
|
+
app: z.string().describe("App name to match, e.g. 'Visual Studio Code'"),
|
|
16
|
+
zone: z.number().int().min(1).describe("1-based zone number, as shown on the drag overlay"),
|
|
17
|
+
title: z.string().optional().describe("Only windows whose title contains this substring"),
|
|
18
|
+
screen: z.number().int().optional().describe("Monitor index; defaults to the one the window is on"),
|
|
19
|
+
}, async (args) => text(await call("/layout/zone", { method: "POST", body: args })));
|
|
20
|
+
server.tool("delete_layout", "Deprecated alias of delete_workspace. Deletes the saved workspace of that name.", { name: z.string() }, async ({ name }) => text(await call("/workspaces/delete", { method: "POST", body: { name } })));
|
|
21
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { call, text } from "../api.js";
|
|
4
|
+
// The interactive modes hand the user a crosshair and wait for them.
|
|
5
|
+
const INTERACTIVE_TIMEOUT_MS = 5 * 60_000;
|
|
6
|
+
// Refuse to inline anything larger; a full retina desktop is easily 10 MB,
|
|
7
|
+
// which is dead weight in the conversation.
|
|
8
|
+
const MAX_INLINE_BYTES = 4 << 20;
|
|
9
|
+
export function register(server) {
|
|
10
|
+
server.tool("take_screenshot", "Capture the screen and return the image so it can be looked at. mode 'screen' captures everything (no user interaction), 'region' and 'window' hand the user the native crosshair/window picker and wait for them. Set annotate=true to open Plonk's drawing editor on the capture instead of returning it — use that when the user wants to mark the shot up themselves. Optional 'path' writes to an explicit file, otherwise the configured screenshot folder is used; 'clipboard' overrides the configured copy-to-clipboard behavior. The returned image is scaled down for legibility; the file at 'path' keeps full resolution. To draw on the result, pass that 'path' to annotate_screenshot.", {
|
|
11
|
+
mode: z.enum(["screen", "region", "window"]).default("screen"),
|
|
12
|
+
annotate: z.boolean().optional().describe("Open the annotation editor instead of returning the image"),
|
|
13
|
+
path: z.string().optional().describe("Explicit output file path (.png)"),
|
|
14
|
+
clipboard: z.boolean().optional().describe("Also copy the capture to the clipboard"),
|
|
15
|
+
include_image: z
|
|
16
|
+
.boolean()
|
|
17
|
+
.optional()
|
|
18
|
+
.describe("Return the image content itself, so it can be inspected (default true)"),
|
|
19
|
+
}, async ({ mode, annotate, path, clipboard, include_image }) => {
|
|
20
|
+
const result = await call("/shot/capture", {
|
|
21
|
+
method: "POST",
|
|
22
|
+
body: { mode, annotate, path, clipboard, preview: include_image !== false },
|
|
23
|
+
timeoutMs: mode === "screen" ? undefined : INTERACTIVE_TIMEOUT_MS,
|
|
24
|
+
});
|
|
25
|
+
const savedPath = "path" in result && typeof result.path === "string" ? result.path : undefined;
|
|
26
|
+
if (!savedPath || include_image === false)
|
|
27
|
+
return text(result);
|
|
28
|
+
const preview = "preview_path" in result && typeof result.preview_path === "string"
|
|
29
|
+
? result.preview_path
|
|
30
|
+
: undefined;
|
|
31
|
+
const imagePath = preview ?? savedPath;
|
|
32
|
+
try {
|
|
33
|
+
const data = await readFile(imagePath);
|
|
34
|
+
if (data.byteLength > MAX_INLINE_BYTES) {
|
|
35
|
+
return text({
|
|
36
|
+
...result,
|
|
37
|
+
note: `image is ${Math.round(data.byteLength / 1024)} KB, too large to inline — read it from 'path' if needed`,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
content: [
|
|
42
|
+
{ type: "text", text: JSON.stringify(result, null, 2) },
|
|
43
|
+
{ type: "image", data: data.toString("base64"), mimeType: "image/png" },
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return text({ ...result, warning: `saved but could not be read back from ${imagePath}` });
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { call, text } from "../api.js";
|
|
2
|
+
export function register(server) {
|
|
3
|
+
server.tool("get_state", "Get the current desktop state: all screens/monitors (index, frame, visible area — coordinates have origin at top-left of the primary screen, y grows down), all open windows (app name, title, which screen it is on, absolute frame, and 'fraction' — its position as fractions 0..1 of that screen's visible area), saved layout names, and whether keep-awake is on. ALWAYS call this first before applying a layout, to see which apps are running and how many monitors there are.", {}, async () => text(await call("/state")));
|
|
4
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { call, text } from "../api.js";
|
|
3
|
+
import { workspaceItemsSchema } from "../schemas.js";
|
|
4
|
+
/// Launching waits for every app to open a window, which the app gives up on
|
|
5
|
+
/// after 20 seconds per app.
|
|
6
|
+
const LAUNCH_TIMEOUT_MS = 90_000;
|
|
7
|
+
export function register(server) {
|
|
8
|
+
server.tool("save_workspace", "Save a workspace: the apps of a desktop setup, where each window goes, and what each app should open. Pass 'items' to describe the arrangement, or omit them to snapshot the windows exactly as they are on screen right now. Saving over an existing name replaces it. Saved workspaces are listed in get_state, with their full contents.", {
|
|
9
|
+
name: z.string().describe("Workspace name, e.g. 'work', 'writing'"),
|
|
10
|
+
items: workspaceItemsSchema.optional(),
|
|
11
|
+
move_existing: z
|
|
12
|
+
.boolean()
|
|
13
|
+
.optional()
|
|
14
|
+
.describe("When true (the default), an app that is already running has its windows moved into place. When false, running apps are left alone and only missing apps are launched."),
|
|
15
|
+
}, async ({ name, items, move_existing }) => text(await call("/workspaces/save", { method: "POST", body: { name, items, move_existing } })));
|
|
16
|
+
server.tool("launch_workspace", "Launch a saved workspace: opens every app that is not running, waits for its windows, and moves them into the saved positions. Each window returns to the monitor it was captured on, so a workspace spanning several displays comes back spanning them. macOS cannot open an app straight into a position, so windows appear first and jump into place. Returns per-app success, and reports apps that never opened a window. Takes up to a minute for a large workspace.", {
|
|
17
|
+
name: z.string(),
|
|
18
|
+
screen: z
|
|
19
|
+
.number()
|
|
20
|
+
.int()
|
|
21
|
+
.optional()
|
|
22
|
+
.describe("Pull the whole workspace onto this monitor instead of the ones it was captured on. Use when a display is no longer attached, or to move a setup to another screen."),
|
|
23
|
+
}, async ({ name, screen }) => text(await call("/workspaces/launch", {
|
|
24
|
+
method: "POST",
|
|
25
|
+
body: { name, screen },
|
|
26
|
+
timeoutMs: LAUNCH_TIMEOUT_MS,
|
|
27
|
+
})));
|
|
28
|
+
server.tool("delete_workspace", "Delete a saved workspace by name. Use this to clean up workspaces you created that are no longer wanted.", { name: z.string() }, async ({ name }) => text(await call("/workspaces/delete", { method: "POST", body: { name } })));
|
|
29
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { call, text } from "../api.js";
|
|
3
|
+
import { zonesSchema } from "../schemas.js";
|
|
4
|
+
export function register(server) {
|
|
5
|
+
server.tool("save_zone_set", "Create or replace a named zone set used for drag snapping. Zones are rectangles {x,y,w,h} as fractions 0..1 of a screen's visible area, origin TOP-LEFT; each zone must stay inside the screen, but zones may overlap each other (the smallest one under the cursor wins). Pass 'screen' to also assign the set to that monitor so it becomes active immediately. Built-in sets already exist: Halves, Thirds, 60 / 40, Quarters, Priority.", {
|
|
6
|
+
name: z.string().describe("Zone set name, e.g. 'coding'"),
|
|
7
|
+
zones: zonesSchema,
|
|
8
|
+
screen: z.number().int().optional().describe("Monitor index to assign this set to (0 = primary)"),
|
|
9
|
+
}, async ({ name, zones, screen }) => text(await call("/zones/save", { method: "POST", body: { name, zones, screen } })));
|
|
10
|
+
server.tool("assign_zone_set", "Assign a zone set (built-in or saved) to a monitor. Omit 'name' to restore the default set (Halves); pass 'edge' for edge snapping instead of zones. Available set names and current assignments are in get_state.", {
|
|
11
|
+
screen: z.number().int().describe("Monitor index (0 = primary)"),
|
|
12
|
+
name: z.string().optional().describe("Zone set name, or 'edge' for edge snapping; omit for the default set"),
|
|
13
|
+
}, async ({ screen, name }) => text(await call("/zones/assign", { method: "POST", body: { screen, name } })));
|
|
14
|
+
server.tool("delete_zone_set", "Delete a saved zone set. Monitors using it fall back to the default set. Built-in sets cannot be deleted.", { name: z.string() }, async ({ name }) => text(await call("/zones/delete", { method: "POST", body: { name } })));
|
|
15
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "plonk-mcp",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "MCP server for Plonk — the Mac window manager your AI agent can drive. Layouts, workspaces, snap zones, keep-awake and screenshots.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/ostapondo/plonk.git",
|
|
10
|
+
"directory": "mcp"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/ostapondo/plonk#readme",
|
|
13
|
+
"bugs": "https://github.com/ostapondo/plonk/issues",
|
|
14
|
+
"keywords": [
|
|
15
|
+
"mcp",
|
|
16
|
+
"model-context-protocol",
|
|
17
|
+
"claude",
|
|
18
|
+
"macos",
|
|
19
|
+
"window-manager",
|
|
20
|
+
"workspaces",
|
|
21
|
+
"screenshots"
|
|
22
|
+
],
|
|
23
|
+
"engines": { "node": ">=18" },
|
|
24
|
+
"files": ["dist"],
|
|
25
|
+
"bin": { "plonk-mcp": "dist/server.js" },
|
|
26
|
+
"main": "dist/server.js",
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsc",
|
|
29
|
+
"prepublishOnly": "tsc",
|
|
30
|
+
"typecheck": "tsc --noEmit"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
34
|
+
"zod": "^3.24.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^18.19.0",
|
|
38
|
+
"typescript": "^5.5.0"
|
|
39
|
+
}
|
|
40
|
+
}
|