@thenavidm/slipway 0.1.9 → 0.1.10

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/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  What changed in Slipway, newest first.
4
4
 
5
+ ## 0.1.10, 2026-10-05: what TikTok needed
6
+
7
+ - **`destructiveOff: "hide"`: a server can take its irreversible tools off the list when they are off.** By default `<PREFIX>_ALLOW_DESTRUCTIVE=0` keeps them listed and refuses each call. With `defaults: { destructiveOff: "hide" }` they leave the MCP tool list and the command list, as read-only mode does with every write, and calling one anyway is refused with the setting to unset. TikTok 1.1 hid its publishing tools this way, and keeps doing so.
8
+ - **A shorter general help, for every server.** An agent reads it first and carries it through every later step. Slipway's settings past the two safety switches are named on the "Also" line with the app's tuning settings, the list formats and `--out` and `--timeout` are left to each command's help and `agent-context`, and the command lines say less. On TikTok it went from 530 tokens to 412, which cut Codex's CLI task by about 350 tokens.
9
+ - **`hidden: true` keeps a command out of the general help**, such as `auth`, the name an older release used for `login`. It still runs, and `agent-context` still lists it.
10
+ - **A refusal no longer doubles a period.** A summary that ends its own sentence, as TikTok's "Publish a video at SELF_ONLY." does, read "About to: Publish a video at SELF_ONLY.." in the refusal and the approval messages.
11
+
5
12
  ## 0.1.9, 2026-10-05: what Substack needed
6
13
 
7
14
  - **A resource can wait for an account.** `listed(env)` leaves a resource out until it returns true. Substack 2.2.3 offered its two resources only once a publication was connected; on Slipway they were always offered, and Claude Code then adds its own two resource tools to every message: 40 tokens in tool search, for reads that could only fail.
package/README.md CHANGED
@@ -550,7 +550,7 @@ Every server reads these, under its own prefix: the app name in capitals, `NOTES
550
550
  | Variable | Default | What it does |
551
551
  |---|---|---|
552
552
  | `<PREFIX>_READ_ONLY` | `0` | `1` hides and refuses every write |
553
- | `<PREFIX>_ALLOW_DESTRUCTIVE` | `1` | `0` keeps writes and refuses the irreversible ones |
553
+ | `<PREFIX>_ALLOW_DESTRUCTIVE` | `1` | `0` keeps writes and refuses the irreversible ones; an app with `defaults: { destructiveOff: "hide" }` also leaves them out of the list |
554
554
  | `<PREFIX>_AUDIT_LOG` | none | File that records every attempted write |
555
555
  | `<PREFIX>_CONFIRM` | `human` | `model` lets `confirm: true` alone confirm, for an agent with no person to ask |
556
556
  | `<PREFIX>_CACHE` | `1` | `0` never answers from the local cache |
@@ -572,9 +572,12 @@ See [CHANGELOG.md](CHANGELOG.md).
572
572
 
573
573
  | Server | Package | Covers |
574
574
  | --- | --- | --- |
575
+ | [Apple Podcasts](https://github.com/thenavidm/apple-podcasts-mcp-cli) | [`@thenavidm/apple-podcasts-mcp-cli`](https://www.npmjs.com/package/@thenavidm/apple-podcasts-mcp-cli) 2.0.0 | The catalog, charts per country, reviews and feeds, your library on this Mac with its transcript excerpts, and analytics for a show you own |
575
576
  | [Bluesky](https://github.com/thenavidm/bluesky-mcp-cli) | [`@thenavidm/bluesky-mcp-cli`](https://www.npmjs.com/package/@thenavidm/bluesky-mcp-cli) 2.0.0 | Posting, threads, replies, the timeline, search, feeds, lists, notifications and the social graph |
577
+ | [Google Photos](https://github.com/thenavidm/google-photos-mcp-cli) | [`@thenavidm/google-photos-mcp-cli`](https://www.npmjs.com/package/@thenavidm/google-photos-mcp-cli) 2.0.0 | The photo picker, uploads, albums and their captions, places and maps, and media this server uploaded, across several Google accounts |
576
578
  | [Mastodon](https://github.com/thenavidm/mastodon-mcp-cli) | [`@thenavidm/mastodon-mcp-cli`](https://www.npmjs.com/package/@thenavidm/mastodon-mcp-cli) 2.0.0 | Posting, editing, threads, timelines, search, lists, notifications and following, on any instance |
577
579
  | [Midjourney](https://github.com/thenavidm/midjourney-mcp-cli) | [`@thenavidm/midjourney-mcp-cli`](https://www.npmjs.com/package/@thenavidm/midjourney-mcp-cli) 2.0.0 | Generating images and video, following jobs, downloads, moodboards, the account's library and the explore feeds, through a signed-in browser |
580
+ | [Substack](https://github.com/thenavidm/substack-mcp-cli) | [`@thenavidm/substack-mcp-cli`](https://www.npmjs.com/package/@thenavidm/substack-mcp-cli) 3.0.0 | Drafts, publishing and scheduling, Notes, subscribers, analytics, tags, comments and researching other publications |
578
581
  | [Teachable](https://github.com/thenavidm/teachable-mcp-cli) | [`@thenavidm/teachable-mcp-cli`](https://www.npmjs.com/package/@thenavidm/teachable-mcp-cli) 3.0.0 | Courses, users, enrollments, pricing, coupons and transactions |
579
582
  | [Threads](https://github.com/thenavidm/threads-mcp-cli) | [`@thenavidm/threads-mcp-cli`](https://www.npmjs.com/package/@thenavidm/threads-mcp-cli) 2.0.0 | Posting, threads, carousels, replies and reply approvals, insights and keyword search |
580
583
  | [ThriveCart](https://github.com/thenavidm/thrivecart-mcp-cli) | [`@thenavidm/thrivecart-mcp-cli`](https://www.npmjs.com/package/@thenavidm/thrivecart-mcp-cli) 3.0.0 | Products and pricing, transactions and revenue, customers, subscriptions and affiliates, across several carts |
package/dist/app.d.ts CHANGED
@@ -90,6 +90,11 @@ export type CliCommand = {
90
90
  * taken as Slipway's global flag of the same name before the command sees it.
91
91
  */
92
92
  flags?: readonly string[];
93
+ /**
94
+ * Left out of the general help, as for the name an older release used for
95
+ * `login`. It still runs, and `agent-context` still lists it.
96
+ */
97
+ hidden?: boolean;
93
98
  run: (io: CliIO, args: string[]) => number | Promise<number>;
94
99
  };
95
100
  export type AppDefinition<Ctx> = {
package/dist/app.js CHANGED
@@ -329,6 +329,11 @@ function assertVisible(tool, policy, envPrefix) {
329
329
  hint: `Unset ${names.readOnly} to allow writes.`,
330
330
  });
331
331
  }
332
+ if (seen.reason === "destructive") {
333
+ throw new RefusedError(`${tool.name} is unavailable: this server is running with ${names.allowDestructive}=0.`, {
334
+ hint: `Unset ${names.allowDestructive} to allow irreversible writes.`,
335
+ });
336
+ }
332
337
  throw new UsageError(`${tool.name} is in a toolset that is off: ${tool.tags.join(", ")}.`, {
333
338
  hint: `Add one of them to ${names.toolsets}, or set ${names.toolsets}=all.`,
334
339
  });
package/dist/cli/help.js CHANGED
@@ -57,12 +57,15 @@ function hiddenNote(app, env) {
57
57
  const off = new Set();
58
58
  let byToolset = 0;
59
59
  let byReadOnly = 0;
60
+ let byDestructive = 0;
60
61
  for (const tool of app.allTools) {
61
62
  const seen = visibility(tool, policy);
62
63
  if (seen.visible)
63
64
  continue;
64
65
  if (seen.reason === "read-only")
65
66
  byReadOnly += 1;
67
+ else if (seen.reason === "destructive")
68
+ byDestructive += 1;
66
69
  else {
67
70
  byToolset += 1;
68
71
  for (const tag of tool.tags)
@@ -77,6 +80,8 @@ function hiddenNote(app, env) {
77
80
  }
78
81
  if (byReadOnly)
79
82
  lines.push(` ${byReadOnly} ${byReadOnly === 1 ? "write is" : "writes are"} hidden by ${names.readOnly}=1.`);
83
+ if (byDestructive)
84
+ lines.push(` ${byDestructive} irreversible ${byDestructive === 1 ? "write is" : "writes are"} hidden by ${names.allowDestructive}=0.`);
80
85
  return lines.length ? [...lines, ``] : [];
81
86
  }
82
87
  export function renderList(app, tools, bin, env = process.env) {
@@ -204,17 +209,18 @@ export function renderGeneralHelp(app, bin) {
204
209
  const sync = app.allTools.some((tool) => tool.sync);
205
210
  const jobs = app.allTools.some((tool) => tool.job);
206
211
  // An agent often reads this first and pays for it again on every later step, so the
207
- // rarely needed commands share one line and Slipway's own settings say only what they do.
212
+ // rarely needed commands share one line, an older name for a command is left out, and
213
+ // Slipway's settings past the two safety switches are named on one line.
208
214
  const commands = [
209
215
  [app.bins.cli, "list the commands"],
210
- [`${bin} <command> --help`, "what one takes, with examples"],
216
+ [`${bin} <command> --help`, "what one takes"],
211
217
  [`${bin} which <words>`, "find the command for a task"],
212
- [`${bin} doctor${app.definition.doctorNetwork ? "" : " [--network]"}`, "check the setup and say what is wrong"],
218
+ [`${bin} doctor${app.definition.doctorNetwork ? "" : " [--network]"}`, "check the setup"],
213
219
  typeof app.definition.login === "object"
214
220
  ? [`${bin} ${app.definition.login.usage ?? "login"}`, app.definition.login.help]
215
221
  : [`${bin} login`, "how to connect an account"],
216
- ...(app.definition.commands ?? []).map((command) => [`${bin} ${command.usage ?? command.name}`, command.help]),
217
- [`${bin} install <client>`, "add the server to an MCP client; install --help lists them"],
222
+ ...(app.definition.commands ?? []).filter((command) => !command.hidden).map((command) => [`${bin} ${command.usage ?? command.name}`, command.help]),
223
+ [`${bin} install <client>`, "add the server to an MCP client"],
218
224
  ...(cache || sync ? [[`${bin} data`, "what is kept on this machine; data clear [<command>] deletes it"]] : []),
219
225
  ...(sync
220
226
  ? [
@@ -223,27 +229,41 @@ export function renderGeneralHelp(app, bin) {
223
229
  [`${bin} data sql "<select>"`, "query local data with read-only SQL"],
224
230
  ]
225
231
  : []),
226
- [app.bins.mcp, "the MCP server over stdio; --http [--port N] for HTTP"],
232
+ [app.bins.mcp, "the MCP server; --http [--port N] serves HTTP"],
233
+ ];
234
+ // Tuning keeps a working default, so it is named on one line with Slipway's own settings past
235
+ // the two safety switches; agent-context says what each does.
236
+ const tuning = [
237
+ ...(app.definition.settings ?? []).filter((setting) => setting.tuning).map((setting) => setting.env),
238
+ names.surface,
239
+ names.auditLog,
240
+ names.toolTimeoutMs,
241
+ names.confirm,
242
+ ...(cache ? [names.cache] : []),
243
+ ...(cache || sync ? [names.dataDir] : []),
244
+ `${app.envPrefix}_DEBUG`,
227
245
  ];
228
- // Tuning keeps a working default, so it is named on one line; agent-context says what each does.
229
- const tuning = (app.definition.settings ?? []).filter((setting) => setting.tuning).map((setting) => setting.env);
230
246
  const settings = [
231
247
  ...(app.definition.settings ?? []).filter((setting) => !setting.tuning).map((setting) => [setting.env, setting.description]),
232
248
  [`${names.readOnly}=1`, "hide and refuse every write"],
233
- [`${names.allowDestructive}=0`, app.allTools.some((tool) => tool.spends) ? "refuse the irreversible writes and paid calls" : "refuse the irreversible writes"],
249
+ [
250
+ `${names.allowDestructive}=0`,
251
+ `${app.definition.defaults?.destructiveOff === "hide" ? "hide and refuse" : "refuse"} the irreversible writes${app.allTools.some((tool) => tool.spends) ? " and paid calls" : ""}`,
252
+ ],
234
253
  // With no tagged tool every tool is always on, so the switch would do nothing.
235
254
  ...(app.allTools.some((tool) => tool.tags.length > 0) ? [[`${names.toolsets}=a,b`, "only these toolsets, or all"]] : []),
236
- [`${names.surface}=search`, "MCP lists three finder tools instead"],
237
- [`${names.auditLog}=<file>`, "log every attempted write"],
238
- [`${names.toolTimeoutMs}=<ms>`, "deadline for any tool"],
239
- [`${names.confirm}=model`, "confirm: true alone confirms over MCP"],
240
- ...(cache ? [[`${names.cache}=0`, "never answer from the local cache"]] : []),
241
- ...(cache || sync ? [[`${names.dataDir}=<dir>`, "keep local data in this folder"]] : []),
242
255
  [`${app.envPrefix}_HTTP_PORT / _HOST / _TOKEN / _ALLOWED_ORIGINS`, "for --http"],
243
- [`${app.envPrefix}_DEBUG=1`, "debug lines on stderr"],
244
256
  ];
245
- // Flags that cannot apply here (jobs, the cache) are left out; agent-context lists every one.
246
- const flags = GLOBAL_FLAGS.map(([flag]) => flag).filter((flag) => flag !== "--agent" && (flag !== "--wait" || jobs) && (flag !== "--refresh" || cache));
257
+ // The list formats appear in the help of the commands that list; flags that cannot apply
258
+ // here (jobs, the cache) are left out. agent-context lists every one.
259
+ const flags = GLOBAL_FLAGS.map(([flag]) => flag).filter((flag) => flag !== "--agent" &&
260
+ flag !== "--jsonl" &&
261
+ flag !== "--csv / --tsv" &&
262
+ flag !== "--quiet" &&
263
+ flag !== "--out <file>" &&
264
+ flag !== "--timeout <ms>" &&
265
+ (flag !== "--wait" || jobs) &&
266
+ (flag !== "--refresh" || cache));
247
267
  const commandRow = table(commands);
248
268
  const settingRow = table(settings);
249
269
  const lines = [
@@ -251,13 +271,13 @@ export function renderGeneralHelp(app, bin) {
251
271
  `${app.title} ${app.version}`,
252
272
  ``,
253
273
  ...commands.map(commandRow),
254
- ` Also: schema <command>, agent-context [--brief] (all of this as JSON), completion <shell>.`,
274
+ ` Also: schema <command>, agent-context (all of this as JSON), completion <shell>.`,
255
275
  ``,
256
276
  `Flags: ${flags.join(", ")}, and --agent: compact JSON, no prompts, never confirms a write.`,
257
277
  ``,
258
278
  `Settings:`,
259
279
  ...settings.map(settingRow),
260
- ...(tuning.length ? [` Also: ${tuning.join(", ")}, described in agent-context.`] : []),
280
+ ` Also: ${tuning.join(", ")}, described in agent-context.`,
261
281
  ``,
262
282
  `Exit codes: ${EXIT.ok} ok, ${EXIT.error} unexpected, ${EXIT.usage} usage or refused, ${EXIT.notFound} not found, ${EXIT.auth} auth, ${EXIT.api} API, ${EXIT.rateLimited} rate limited, ${EXIT.notConfigured} not configured`,
263
283
  ``,
package/dist/cli/run.js CHANGED
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import { writeFileSync } from "node:fs";
10
10
  import { runDoctor } from "../doctor.js";
11
- import { EXIT, UsageError, toSlipwayError } from "../errors.js";
11
+ import { EXIT, RefusedError, UsageError, toSlipwayError } from "../errors.js";
12
12
  import { eachPage } from "../pages.js";
13
13
  import { visibility, policyEnvNames } from "../policy.js";
14
14
  import { outputJsonSchema } from "../schema.js";
@@ -169,6 +169,12 @@ export async function runCli(app, argv, partial = {}) {
169
169
  });
170
170
  }
171
171
  const seen = visibility(tool, app.policy(io.env));
172
+ if (!seen.visible && seen.reason === "destructive") {
173
+ const names = policyEnvNames(app.envPrefix);
174
+ throw new RefusedError(`${tool.command} is unavailable: ${names.allowDestructive}=0 hides the irreversible writes.`, {
175
+ hint: `Unset ${names.allowDestructive} to allow them.`,
176
+ });
177
+ }
172
178
  if (!seen.visible) {
173
179
  const names = policyEnvNames(app.envPrefix);
174
180
  throw new UsageError(seen.reason === "read-only"
package/dist/confirm.js CHANGED
@@ -20,7 +20,7 @@
20
20
  import { randomBytes, randomUUID } from "node:crypto";
21
21
  import { CLIENT_CAPABILITIES_META_KEY, CLIENT_INFO_META_KEY, createRequestStateCodec, inputRequired, inputResponse, } from "@modelcontextprotocol/server";
22
22
  import { RefusedError } from "./errors.js";
23
- import { consequence, Guard } from "./guard.js";
23
+ import { consequence, Guard, sentenceBody } from "./guard.js";
24
24
  import { phrase, sha256, stableJson, versionAtLeast } from "./util.js";
25
25
  /** The key of Slipway's approval form among a call's input requests. */
26
26
  export const APPROVAL_KEY = "slipway_approval";
@@ -149,18 +149,18 @@ export async function personApproval(app, tool, rawArgs, ctx, env) {
149
149
  return undefined;
150
150
  if (answer.kind === "elicit" && answer.action === "accept") {
151
151
  guard.record(tool, summary, "blocked: person declined");
152
- throw new RefusedError(`The approval form was accepted without ticking Approve, so ${tool.name} did not run. About to: ${summary}.`, {
152
+ throw new RefusedError(`The approval form was accepted without ticking Approve, so ${tool.name} did not run. About to: ${sentenceBody(summary)}.`, {
153
153
  hint: "Ask the user whether they want this, and call again only if they do.",
154
154
  });
155
155
  }
156
156
  if (answer.kind === "elicit" && answer.action === "cancel") {
157
157
  guard.record(tool, summary, "blocked: no answer");
158
- throw new RefusedError(`The approval form was closed without an answer, so ${tool.name} did not run. About to: ${summary}.`, {
158
+ throw new RefusedError(`The approval form was closed without an answer, so ${tool.name} did not run. About to: ${sentenceBody(summary)}.`, {
159
159
  hint: "Ask the user whether they want this, and call again only if they do.",
160
160
  });
161
161
  }
162
162
  guard.record(tool, summary, "blocked: person declined");
163
- throw new RefusedError(`The approval was declined, so ${tool.name} did not run. About to: ${summary}.`, {
163
+ throw new RefusedError(`The approval was declined, so ${tool.name} did not run. About to: ${sentenceBody(summary)}.`, {
164
164
  hint: "Do not call it again unless the user asks for it.",
165
165
  });
166
166
  }
package/dist/guard.d.ts CHANGED
@@ -44,3 +44,5 @@ export declare class Guard {
44
44
  }
45
45
  /** Why a tool needs confirming, in the words a refusal and an approval form both use. */
46
46
  export declare function consequence(tool: Pick<Tool, "risk" | "consequence" | "spends">): string;
47
+ /** A summary that ends its own sentence, "Publish a video.", is not given a second period. */
48
+ export declare function sentenceBody(text: string): string;
package/dist/guard.js CHANGED
@@ -58,7 +58,7 @@ export class Guard {
58
58
  }
59
59
  if (tool.requireConfirm && !options.confirmedBy) {
60
60
  this.record(tool, options.summary, "blocked: no confirm");
61
- throw new RefusedError(`${tool.name} ${consequence(tool)}, so it will not run without ${this.confirmFlag}. About to: ${options.summary}. Call again with ${this.confirmFlag} if that is what was asked for.`, { hint: `Pass ${this.confirmFlag} only when the user asked for this exact action.` });
61
+ throw new RefusedError(`${tool.name} ${consequence(tool)}, so it will not run without ${this.confirmFlag}. About to: ${sentenceBody(options.summary)}. Call again with ${this.confirmFlag} if that is what was asked for.`, { hint: `Pass ${this.confirmFlag} only when the user asked for this exact action.` });
62
62
  }
63
63
  this.record(tool, options.summary, "allowed", options.confirmedBy);
64
64
  }
@@ -91,3 +91,7 @@ export function consequence(tool) {
91
91
  return "spends money and cannot be refunded";
92
92
  return tool.risk === "destructive" ? "is public or cannot be undone" : "has an effect that cannot be taken back";
93
93
  }
94
+ /** A summary that ends its own sentence, "Publish a video.", is not given a second period. */
95
+ export function sentenceBody(text) {
96
+ return text.trim().replace(/[.!?]+$/, "");
97
+ }
package/dist/policy.d.ts CHANGED
@@ -31,6 +31,8 @@ export type Policy = {
31
31
  confirm: ConfirmMode;
32
32
  /** Whether reads that opted in may answer from the local cache. */
33
33
  cache: boolean;
34
+ /** Whether the irreversible tools and paid calls are left out of the list, not only refused. */
35
+ hideDestructive: boolean;
34
36
  };
35
37
  export type PolicyDefaults = {
36
38
  /**
@@ -41,6 +43,13 @@ export type PolicyDefaults = {
41
43
  toolsets?: readonly string[] | "all" | ((env: NodeJS.ProcessEnv) => readonly string[] | "all");
42
44
  surface?: ToolSurface;
43
45
  confirm?: ConfirmMode;
46
+ /**
47
+ * What `<PREFIX>_ALLOW_DESTRUCTIVE=0` does to the irreversible tools and paid
48
+ * calls: `refuse` each call (the default), or `hide` them from the list, as
49
+ * read-only mode hides every write. A server that hid them before it moved
50
+ * keeps doing so.
51
+ */
52
+ destructiveOff?: "refuse" | "hide";
44
53
  };
45
54
  export type PolicyEnv = {
46
55
  readOnly: string;
@@ -59,10 +68,12 @@ export type Visibility = {
59
68
  visible: true;
60
69
  } | {
61
70
  visible: false;
62
- reason: "read-only" | "toolset";
71
+ reason: "read-only" | "destructive" | "toolset";
63
72
  };
64
73
  /** Whether a tool is on under this policy, and if not, why, so a refusal can say how to turn it on. */
65
- export declare function visibility(tool: Pick<Tool, "risk" | "tags">, policy: Policy): Visibility;
74
+ export declare function visibility(tool: Pick<Tool, "risk" | "tags"> & {
75
+ spends?: boolean;
76
+ }, policy: Policy): Visibility;
66
77
  export declare function riskMark(tool: {
67
78
  risk: Risk;
68
79
  spends?: boolean;
package/dist/policy.js CHANGED
@@ -47,21 +47,25 @@ export function readPolicy(env, prefix, defaults = {}) {
47
47
  const surface = env[names.surface]?.trim().toLowerCase();
48
48
  const timeout = Number(env[names.toolTimeoutMs]);
49
49
  const confirm = env[names.confirm]?.trim().toLowerCase();
50
+ const allowDestructive = flag(env[names.allowDestructive], true);
50
51
  return {
51
52
  readOnly: flag(env[names.readOnly], false),
52
- allowDestructive: flag(env[names.allowDestructive], true),
53
+ allowDestructive,
53
54
  auditLog: env[names.auditLog]?.trim() || undefined,
54
55
  toolsets,
55
56
  surface: surface === "search" || surface === "full" ? surface : (defaults.surface ?? "full"),
56
57
  toolTimeoutMs: Number.isFinite(timeout) && timeout > 0 ? timeout : undefined,
57
58
  confirm: confirm === "human" || confirm === "model" ? confirm : (defaults.confirm ?? "human"),
58
59
  cache: flag(env[names.cache], true),
60
+ hideDestructive: !allowDestructive && defaults.destructiveOff === "hide",
59
61
  };
60
62
  }
61
63
  /** Whether a tool is on under this policy, and if not, why, so a refusal can say how to turn it on. */
62
64
  export function visibility(tool, policy) {
63
65
  if (policy.readOnly && tool.risk !== "read")
64
66
  return { visible: false, reason: "read-only" };
67
+ if (policy.hideDestructive && (tool.risk === "destructive" || tool.spends === true))
68
+ return { visible: false, reason: "destructive" };
65
69
  if (policy.toolsets !== "all" && tool.tags.length > 0) {
66
70
  const on = policy.toolsets;
67
71
  if (!tool.tags.some((tag) => on.has(tag)))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thenavidm/slipway",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "description": "Slipway, the TypeScript framework for MCP servers and agent-native CLIs. One tool definition ships an MCP server and a CLI, with write safety, typed results and release checks built in.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",