@thenavidm/slipway 0.1.0
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 +21 -0
- package/LICENSE +202 -0
- package/NOTICE +4 -0
- package/README.md +757 -0
- package/SECURITY.md +33 -0
- package/SKILL.md +136 -0
- package/dist/app.d.ts +210 -0
- package/dist/app.js +364 -0
- package/dist/bin.d.ts +14 -0
- package/dist/bin.js +178 -0
- package/dist/check.d.ts +52 -0
- package/dist/check.js +313 -0
- package/dist/cli/completion.d.ts +5 -0
- package/dist/cli/completion.js +72 -0
- package/dist/cli/context.d.ts +97 -0
- package/dist/cli/context.js +94 -0
- package/dist/cli/data.d.ts +17 -0
- package/dist/cli/data.js +120 -0
- package/dist/cli/flags.d.ts +35 -0
- package/dist/cli/flags.js +208 -0
- package/dist/cli/help.d.ts +15 -0
- package/dist/cli/help.js +213 -0
- package/dist/cli/install.d.ts +10 -0
- package/dist/cli/install.js +65 -0
- package/dist/cli/output.d.ts +22 -0
- package/dist/cli/output.js +159 -0
- package/dist/cli/run.d.ts +10 -0
- package/dist/cli/run.js +343 -0
- package/dist/confirm.d.ts +90 -0
- package/dist/confirm.js +166 -0
- package/dist/data.d.ts +109 -0
- package/dist/data.js +324 -0
- package/dist/docs.d.ts +13 -0
- package/dist/docs.js +66 -0
- package/dist/doctor.d.ts +12 -0
- package/dist/doctor.js +100 -0
- package/dist/entry.d.ts +11 -0
- package/dist/entry.js +34 -0
- package/dist/errors.d.ts +96 -0
- package/dist/errors.js +153 -0
- package/dist/guard.d.ts +46 -0
- package/dist/guard.js +89 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +16 -0
- package/dist/install.d.ts +98 -0
- package/dist/install.js +325 -0
- package/dist/jobs.d.ts +143 -0
- package/dist/jobs.js +247 -0
- package/dist/openapi.d.ts +127 -0
- package/dist/openapi.js +549 -0
- package/dist/pages.d.ts +22 -0
- package/dist/pages.js +61 -0
- package/dist/policy.d.ts +66 -0
- package/dist/policy.js +74 -0
- package/dist/redact.d.ts +18 -0
- package/dist/redact.js +60 -0
- package/dist/result.d.ts +44 -0
- package/dist/result.js +74 -0
- package/dist/rpc.d.ts +69 -0
- package/dist/rpc.js +120 -0
- package/dist/schema.d.ts +99 -0
- package/dist/schema.js +192 -0
- package/dist/search.d.ts +18 -0
- package/dist/search.js +119 -0
- package/dist/serve.d.ts +30 -0
- package/dist/serve.js +149 -0
- package/dist/server.d.ts +20 -0
- package/dist/server.js +244 -0
- package/dist/sync.d.ts +35 -0
- package/dist/sync.js +119 -0
- package/dist/testing.d.ts +35 -0
- package/dist/testing.js +41 -0
- package/dist/tool.d.ts +194 -0
- package/dist/tool.js +151 -0
- package/dist/util.d.ts +12 -0
- package/dist/util.js +35 -0
- package/package.json +89 -0
package/README.md
ADDED
|
@@ -0,0 +1,757 @@
|
|
|
1
|
+
<picture>
|
|
2
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://cdn.navid.me/repos/slipway-logo-light.png">
|
|
3
|
+
<img src="https://cdn.navid.me/repos/slipway-logo-dark.png" alt="Slipway" width="88">
|
|
4
|
+
</picture>
|
|
5
|
+
|
|
6
|
+
# Slipway: MCP Server & CLI Framework
|
|
7
|
+
|
|
8
|
+
[](https://www.npmjs.com/package/@thenavidm/slipway)
|
|
9
|
+
[](https://github.com/thenavidm/slipway/actions/workflows/ci.yml)
|
|
10
|
+
[](./LICENSE)
|
|
11
|
+
[](https://youtube.com/@thenavidm?sub_confirmation=1)
|
|
12
|
+
[](https://x.com/thenavidm)
|
|
13
|
+
[](https://linkedin.com/in/thenavidm)
|
|
14
|
+
|
|
15
|
+
TypeScript framework for MCP servers and CLIs, for Claude Code, Codex and AI agents. Describe each tool once and Slipway ships it as an MCP server tool and a command line command, with write safety, typed results and release checks built in.
|
|
16
|
+
|
|
17
|
+
The two surfaces cannot drift apart. They are generated from the same tool list, and every call on either one runs through the same code: the same validation, the same guard, the same errors.
|
|
18
|
+
|
|
19
|
+
Built and maintained by [Navid Moazzez](https://navid.me?utm_source=github&utm_medium=referral&utm_campaign=slipway&utm_content=readme).
|
|
20
|
+
|
|
21
|
+
## Two ways to use it
|
|
22
|
+
|
|
23
|
+
Every server built on Slipway ships both, from one definition like this:
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { defineTool, slipway, z } from "@thenavidm/slipway";
|
|
27
|
+
|
|
28
|
+
const deleteNote = defineTool({
|
|
29
|
+
name: "delete_note",
|
|
30
|
+
title: "Delete a note",
|
|
31
|
+
description: "Delete a note forever. There is no undo and no trash.",
|
|
32
|
+
input: z.object({ id: z.number().int().describe("The note id.") }),
|
|
33
|
+
risk: "destructive",
|
|
34
|
+
positional: ["id"],
|
|
35
|
+
summary: ({ id }) => `delete note ${id}`,
|
|
36
|
+
handler: ({ id }, ctx) => ctx.api.deleteNote(id),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export const app = slipway({
|
|
40
|
+
name: "notes",
|
|
41
|
+
version: "1.0.0",
|
|
42
|
+
context: (env) => ({ api: new NotesApi(env.NOTES_API_KEY) }),
|
|
43
|
+
tools: [deleteNote],
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Command line
|
|
48
|
+
|
|
49
|
+
`<name>-cli` runs every tool as a command, with flags derived from the same JSON Schema the model reads. It is built for agents as much as for people: one flag for machine output, exit codes a script can branch on, field selection, dry runs and a full description of itself as JSON.
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
notes-cli # every command, writes marked
|
|
53
|
+
notes-cli which remove a note # find the command for a task
|
|
54
|
+
notes-cli get-note 7 --select title --compact
|
|
55
|
+
notes-cli list-notes --all --jsonl # follow every page, one record per line
|
|
56
|
+
notes-cli delete-note 7 # refused: irreversible, so it needs --confirm
|
|
57
|
+
notes-cli delete-note 7 --dry-run # what would run, without running it
|
|
58
|
+
notes-cli agent-context # commands, flags, risk and exit codes as JSON
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### MCP server, for your AI app
|
|
62
|
+
|
|
63
|
+
`<name>-mcp` is what Claude Code, Codex, Claude Desktop and Cursor launch. It serves MCP over stdio, or over Streamable HTTP with `--http`, and answers clients on the 2025 protocol and on the 2026-07-28 revision from one server. `<name>-cli install codex` adds it to a client in one step.
|
|
64
|
+
|
|
65
|
+
Every tool goes out with the annotations its risk implies, so a client that auto-approves reads or prompts on writes gets an honest answer from every tool. An irreversible call waits for a person to approve it, wherever the client can ask one.
|
|
66
|
+
|
|
67
|
+
### Which one
|
|
68
|
+
|
|
69
|
+
| Where you are | What you can reach |
|
|
70
|
+
|---|---|
|
|
71
|
+
| An agent that runs shell commands, like Claude Code or Codex | Both. The CLI costs nothing until a command runs |
|
|
72
|
+
| A chat app with no shell, like claude.ai or Claude Desktop | The MCP server only |
|
|
73
|
+
| A terminal, a script, cron or CI | The CLI only |
|
|
74
|
+
|
|
75
|
+
They are the same program reading the same tool definitions, so anything one can do, the other can.
|
|
76
|
+
|
|
77
|
+
## Features
|
|
78
|
+
|
|
79
|
+
- **One definition, two surfaces.** A tool added today is a command today, under the same name, with the same arguments.
|
|
80
|
+
- **Write safety that holds on both surfaces.** Three risk levels, confirmation for irreversible calls, a read-only switch, a switch that blocks irreversible writes, and an audit log. Agent mode never confirms anything.
|
|
81
|
+
- **Confirmation a model cannot fake.** A person approves every irreversible call: in Claude Code's own prompt, in an approval form in any other client that can show one, and through the model's `confirm: true` only where a client can do neither. An approval is signed, bound to the exact call, and works once.
|
|
82
|
+
- **Long-running jobs.** A tool that starts a render or an export waits a bounded time, then hands back a job to check with a generated status tool, so no client gives up on it. In a terminal, `--wait` waits to the end.
|
|
83
|
+
- **Local data.** Reads can opt in to a cache, kept per account and cleared by any write. `data sync`, `data search` and `data sql` keep an offline, searchable copy of any list, in one private SQLite file with nothing to install.
|
|
84
|
+
- **OpenAPI to tools.** `fromOpenAPI()` turns every operation in a document into a tool, with the risk its method implies, its tags as toolsets, and a hash pin that refuses a changed document. It turns 611 of the 612 operations in Stripe's API into tools, and 1,230 of GitHub's 1,232; the rest are file uploads or raw text.
|
|
85
|
+
- **One command to install.** `<cli> install codex`, or `claude-code`, `claude-desktop`, `cursor`, `vscode` or `gemini`, adds the server to that client's own configuration and passes credentials on instead of writing them down.
|
|
86
|
+
- **Typed results.** Declare an output schema and results go out as validated `structuredContent`. Object results are structured even without one.
|
|
87
|
+
- **Contract tools.** A tool built from a pinned JSON contract joins the same list as a hand-written Zod tool, with `jsonSchema({...})`.
|
|
88
|
+
- **Toolsets and a search surface** for large catalogs, so a client loads only what a person turns on.
|
|
89
|
+
- **Typed errors and exit codes.** Every error carries its exit code and a hint: JSON on stderr in a terminal, a readable error result over MCP.
|
|
90
|
+
- **Secrets stay out.** Registered credentials, and any field named like one, are masked in every result and error.
|
|
91
|
+
- **A release gate.** `slipway check` tests schemas, sizes, examples, MCP and CLI parity, the commands your docs mention, and that the built server starts with nothing configured.
|
|
92
|
+
- **Light.** Two runtime dependencies: the official MCP SDK and Zod. Local data uses the SQLite built into Node.js.
|
|
93
|
+
|
|
94
|
+
## Contents
|
|
95
|
+
|
|
96
|
+
| # | Section | What it covers |
|
|
97
|
+
|---|---|---|
|
|
98
|
+
| 1 | [Install](#1-install) | Requirements and the local install |
|
|
99
|
+
| 2 | [Build a server](#2-build-a-server) | The three files every server needs |
|
|
100
|
+
| 3 | [Tools](#3-tools) | Every field of `defineTool`, results, errors, contract tools |
|
|
101
|
+
| 4 | [Safety](#4-safety) | Risk levels, approval by a person, read-only mode, the audit log |
|
|
102
|
+
| 5 | [Long-running jobs](#5-long-running-jobs) | Jobs, status tools and waiting |
|
|
103
|
+
| 6 | [Local data](#6-local-data) | The cache, synced lists, offline search and SQL |
|
|
104
|
+
| 7 | [OpenAPI](#7-openapi) | Every operation in a document as a tool, pinned |
|
|
105
|
+
| 8 | [The CLI](#8-the-cli) | Commands, flags, output shapes and exit codes |
|
|
106
|
+
| 9 | [The MCP server](#9-the-mcp-server) | Transports, HTTP security, resources and prompts |
|
|
107
|
+
| 10 | [Add it to a client](#10-add-it-to-a-client) | `install` for six clients, and what each does with credentials |
|
|
108
|
+
| 11 | [Large catalogs](#11-large-catalogs) | Toolsets and the search surface |
|
|
109
|
+
| 12 | [Release checks](#12-release-checks) | `slipway check`, `docs`, `inspect` and `openapi` |
|
|
110
|
+
| 13 | [Testing](#13-testing) | In-memory MCP and CLI helpers, both protocol eras |
|
|
111
|
+
| 14 | [Troubleshooting](#14-troubleshooting) | Symptoms, causes and fixes |
|
|
112
|
+
| 15 | [FAQ](#15-faq-) | Twenty-five questions, answered |
|
|
113
|
+
|
|
114
|
+
## 1. Install
|
|
115
|
+
|
|
116
|
+
Slipway needs Node.js 22 or later, and ESM. Local data uses the SQLite built into Node.js 22.13 and later; on an older release everything else works and the cache stays off.
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
npm install @thenavidm/slipway
|
|
120
|
+
npm install --save-dev ajv
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`ajv` is optional and only used by `slipway check`, to validate schemas against JSON Schema 2020-12, the check Claude Code runs before it accepts a tool. It never ships to your users.
|
|
124
|
+
|
|
125
|
+
## 2. Build a server
|
|
126
|
+
|
|
127
|
+
A server is three files.
|
|
128
|
+
|
|
129
|
+
**`src/tools.ts`** says what the server can do. `toolkit<Context>()` binds the context type once, so every handler gets `ctx.api` typed:
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
import { toolkit, z } from "@thenavidm/slipway";
|
|
133
|
+
import type { Context } from "./app.js";
|
|
134
|
+
|
|
135
|
+
const { defineTool } = toolkit<Context>();
|
|
136
|
+
|
|
137
|
+
export const getNote = defineTool({
|
|
138
|
+
name: "get_note",
|
|
139
|
+
title: "Get a note",
|
|
140
|
+
description: "Read one note by its id, with its full body.",
|
|
141
|
+
input: z.object({ id: z.number().int().min(1).describe("The note id.") }),
|
|
142
|
+
output: z.object({ id: z.number(), title: z.string(), body: z.string() }),
|
|
143
|
+
risk: "read",
|
|
144
|
+
positional: ["id"],
|
|
145
|
+
examples: [{ description: "Read note 7", args: { id: 7 } }],
|
|
146
|
+
handler: ({ id }, ctx) => ctx.api.getNote(id, { signal: ctx.signal }),
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**`src/app.ts`** describes the server and never starts it, so tests and `slipway check` can import it:
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
import { slipway } from "@thenavidm/slipway";
|
|
154
|
+
import { NotesApi } from "./api.js";
|
|
155
|
+
import { getNote } from "./tools.js";
|
|
156
|
+
|
|
157
|
+
export type Context = { api: NotesApi; key?: string };
|
|
158
|
+
|
|
159
|
+
export const app = slipway<Context>({
|
|
160
|
+
name: "notes",
|
|
161
|
+
title: "Notes",
|
|
162
|
+
version: "1.0.0",
|
|
163
|
+
instructions: "Notes: read and manage notes. delete_note needs confirm: true.",
|
|
164
|
+
context: (env) => ({ api: new NotesApi(env.NOTES_API_KEY), key: env.NOTES_API_KEY }),
|
|
165
|
+
configured: (ctx) => Boolean(ctx.key),
|
|
166
|
+
secrets: (ctx) => [ctx.key],
|
|
167
|
+
settings: [{ env: "NOTES_API_KEY", description: "A key from the notes dashboard.", secret: true }],
|
|
168
|
+
login: "Set NOTES_API_KEY to a key from the notes dashboard.",
|
|
169
|
+
tools: [getNote],
|
|
170
|
+
});
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**`src/index.ts`** is both binaries:
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
#!/usr/bin/env node
|
|
177
|
+
import { app } from "./app.js";
|
|
178
|
+
|
|
179
|
+
await app.main();
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
```json
|
|
183
|
+
{
|
|
184
|
+
"bin": { "notes-mcp": "dist/index.js", "notes-cli": "dist/index.js" }
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
`notes-mcp` with no arguments serves MCP over stdio and stays silent on stdout. `notes-cli` with no arguments lists the commands. Any argument on either binary is a command, so a typo is reported instead of starting a server that waits on stdin.
|
|
189
|
+
|
|
190
|
+
The context is built on the first call that needs it, never at startup. `--help` works with nothing configured, and the server answers a client at once and explains what is missing instead of exiting.
|
|
191
|
+
|
|
192
|
+
## 3. Tools
|
|
193
|
+
|
|
194
|
+
| Field | What it does |
|
|
195
|
+
|---|---|
|
|
196
|
+
| `name` | snake_case. The MCP tool name; the CLI command is the same name with dashes |
|
|
197
|
+
| `title` | A few words for pickers and the command list |
|
|
198
|
+
| `description` | What it does and when to use it. The only documentation a model reads before calling |
|
|
199
|
+
| `input` | A Zod object, or `jsonSchema({...})` for a tool generated from an API contract |
|
|
200
|
+
| `output` | Optional. Results are validated against it and sent as `structuredContent` |
|
|
201
|
+
| `risk` | `read`, `write` (easy to undo) or `destructive` (public, irreversible, or both) |
|
|
202
|
+
| `requireConfirm` | Defaults to true for destructive tools. Set it on a write that spends money |
|
|
203
|
+
| `idempotent`, `openWorld` | Annotation hints. Reads are idempotent by default; every tool is open world unless it never leaves the machine |
|
|
204
|
+
| `tags` | Toolsets this tool belongs to. A tool with no tags is always on |
|
|
205
|
+
| `summary` | One line for the refusal message and the audit log: "delete note 7" |
|
|
206
|
+
| `preview` | What `--dry-run` prints. Defaults to the validated arguments |
|
|
207
|
+
| `examples` | Arguments as a client sends them. Shown as runnable commands in help and checked by `slipway check` |
|
|
208
|
+
| `positional` | Inputs that may be typed as bare words, in order |
|
|
209
|
+
| `paginate` | How the tool pages, so `--all` and `--max-items` can follow every page |
|
|
210
|
+
| `job` | The tool starts work that outlasts a call. See [Long-running jobs](#5-long-running-jobs) |
|
|
211
|
+
| `cache` | `{ ttlSeconds }`: keep this read's results locally for a while. See [Local data](#6-local-data) |
|
|
212
|
+
| `sync` | `{ id }`: this read lists records worth an offline copy |
|
|
213
|
+
| `timeoutMs` | Abort the call after this long |
|
|
214
|
+
| `maxResultChars` | This tool's results are legitimately large; raises Claude Code's limit for it |
|
|
215
|
+
| `render` | Text for the result when JSON is not the best way to read it |
|
|
216
|
+
| `handler` | `(args, ctx) => result`. `ctx` is your context plus `signal`, `surface`, `env`, `progress`, `log` and `secrets` |
|
|
217
|
+
|
|
218
|
+
A handler returns plain data. An object goes out as compact JSON text and as typed `structuredContent`. Return `content([...], data)` with `image()`, `audio()`, `file()` or `resourceLink()` for anything that is not text.
|
|
219
|
+
|
|
220
|
+
Throw one of the error classes and the caller gets its exit code. `httpError(status, message)` maps an HTTP status in one line, and `UsageError`, `NotFoundError`, `AuthError`, `RateLimitError`, `ApiError` and `NotConfiguredError` cover the rest.
|
|
221
|
+
|
|
222
|
+
A tool built from a pinned contract joins the same list. To make a tool of every operation in an API, see [OpenAPI](#7-openapi).
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
import { defineTool, jsonSchema } from "@thenavidm/slipway";
|
|
226
|
+
|
|
227
|
+
export const renameCourse = defineTool({
|
|
228
|
+
name: "rename_course",
|
|
229
|
+
title: "Rename a course",
|
|
230
|
+
description: "Change a course's name. Generated from the Admin API contract.",
|
|
231
|
+
input: jsonSchema<{ id: number; name: string }>({
|
|
232
|
+
type: "object",
|
|
233
|
+
properties: { id: { type: "integer", format: "int32" }, name: { type: "string" } },
|
|
234
|
+
required: ["id", "name"],
|
|
235
|
+
additionalProperties: false,
|
|
236
|
+
}),
|
|
237
|
+
risk: "write",
|
|
238
|
+
handler: ({ id, name }, ctx) => ctx.api.patch(`/courses/${id}`, { name }),
|
|
239
|
+
});
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## 4. Safety
|
|
243
|
+
|
|
244
|
+
Shipping no writes is not safety: it hands the work back to a person. Shipping them unguarded is worse. So every write works, and the irreversible ones need a confirmation the caller gives on purpose.
|
|
245
|
+
|
|
246
|
+
| Risk | Example | Guard |
|
|
247
|
+
|---|---|---|
|
|
248
|
+
| `read` | List posts | None |
|
|
249
|
+
| `write` | Like a post, add a label | Off in read-only mode |
|
|
250
|
+
| `destructive` | Publish, delete, block | Needs confirming. Off in read-only mode, and off when irreversible writes are switched off |
|
|
251
|
+
|
|
252
|
+
`confirm: true` is something a model types, so on its own it proves only that the model meant it. Over MCP, Slipway asks the person instead, wherever the client can ask one:
|
|
253
|
+
|
|
254
|
+
| Client | Who confirms an irreversible call |
|
|
255
|
+
|---|---|
|
|
256
|
+
| Claude Code 2.1.246 and later | The person, in Claude Code's own approval prompt, which it shows on every call in every permission mode |
|
|
257
|
+
| Any other client that supports elicitation, such as Codex | The person, in an approval form: the call runs only if they tick Approve and accept |
|
|
258
|
+
| A client that can do neither | The model, with `confirm: true` |
|
|
259
|
+
|
|
260
|
+
The form's answer comes back from the client as data, so Slipway counts it only next to the state it signed when it asked, which names the exact tool and arguments and works once. A client cannot approve a call nobody was asked about, reuse an approval, or move one to other arguments. The form's one field starts unticked and only an explicit yes counts, so a client that answers forms by itself cannot approve anything.
|
|
261
|
+
|
|
262
|
+
In a terminal, `--confirm` on the command itself confirms. The refusal names the exact thing to type on the surface the caller is on, and says what was about to happen, from the tool's `summary`.
|
|
263
|
+
|
|
264
|
+
`--agent` turns on JSON, compact output, no prompts and no color. It never confirms anything, and neither does `--yes`, which is accepted only so scripts written for other tools keep working. The agent that sets those flags is exactly the caller confirmation exists for.
|
|
265
|
+
|
|
266
|
+
Three switches belong to whoever runs the server, under the server's own prefix:
|
|
267
|
+
|
|
268
|
+
| Setting | Effect |
|
|
269
|
+
|---|---|
|
|
270
|
+
| `<PREFIX>_READ_ONLY=1` | Every write disappears from both surfaces, and a direct call is refused |
|
|
271
|
+
| `<PREFIX>_ALLOW_DESTRUCTIVE=0` | Writes stay, irreversible ones are refused |
|
|
272
|
+
| `<PREFIX>_AUDIT_LOG=<file>` | Every attempted write is appended to this file, with its outcome and who confirmed it |
|
|
273
|
+
| `<PREFIX>_CONFIRM=model` | `confirm: true` alone confirms, and nobody is asked. For an agent with no person to ask |
|
|
274
|
+
|
|
275
|
+
A headless agent has nobody to answer an approval. Claude Code run with `-p` refuses a tool that needs a person, and Codex run with `exec` declines the form. Set `<PREFIX>_CONFIRM=model` for those runs, and keep `<PREFIX>_READ_ONLY=1` for any agent that should never write.
|
|
276
|
+
|
|
277
|
+
## 5. Long-running jobs
|
|
278
|
+
|
|
279
|
+
A client stops waiting on a tool after about a minute, and Codex after 60 seconds by default. A render, an export or a long sync cannot simply run inside one call. Declare `job` and Slipway adds a `wait_seconds` argument, waits that long, and generates `<name>_status` to check on the job later.
|
|
280
|
+
|
|
281
|
+
A job the service runs, with its own status endpoint:
|
|
282
|
+
|
|
283
|
+
```ts
|
|
284
|
+
export const renderVideo = defineTool({
|
|
285
|
+
name: "render_video",
|
|
286
|
+
title: "Render a video",
|
|
287
|
+
description: "Render a video from a script. Rendering takes several minutes.",
|
|
288
|
+
input: z.object({ script: z.string().describe("What the video says.") }),
|
|
289
|
+
risk: "write",
|
|
290
|
+
job: {
|
|
291
|
+
id: "id",
|
|
292
|
+
status: (id, ctx) => ctx.api.getRender(id, { signal: ctx.signal }),
|
|
293
|
+
done: (render) => render.state === "done" || render.state === "failed",
|
|
294
|
+
failed: (render) => render.state === "failed",
|
|
295
|
+
progress: (render) => ({ progress: render.percent, total: 100 }),
|
|
296
|
+
},
|
|
297
|
+
handler: ({ script }, ctx) => ctx.api.startRender(script),
|
|
298
|
+
});
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
A handler that is slow on its own runs in the background with `job: { background: true }`. Slipway keeps its result for an hour, in the server that ran it.
|
|
302
|
+
|
|
303
|
+
| What the caller sees | When |
|
|
304
|
+
|---|---|
|
|
305
|
+
| `{ job_id, done: true, status }`, or `result` for a background job | The job finished within the wait |
|
|
306
|
+
| `{ job_id, done: false, status, check }` | It is still running. `check` says how to ask again |
|
|
307
|
+
| An error with the service's status as its details | `failed` says the job failed |
|
|
308
|
+
|
|
309
|
+
`wait_seconds` defaults to 25 and stops at 55, under the minute clients allow. Progress reaches a client that asked for it while a call waits. In a terminal, `--wait` waits to the end however long it takes, and a background job always does, since the command's process is all that keeps it alive.
|
|
310
|
+
|
|
311
|
+
```bash
|
|
312
|
+
notes-cli render-video --script "Hello" --wait
|
|
313
|
+
notes-cli render-video-status r_123 --wait
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
## 6. Local data
|
|
317
|
+
|
|
318
|
+
An agent that asks the same question twice should not pay the service twice, and a person searching 10,000 records should not page through an API to do it. Each app keeps one SQLite file on this machine for both. The folder is readable by its owner only, and so is the file.
|
|
319
|
+
|
|
320
|
+
**The cache.** A read with `cache: { ttlSeconds: 300 }` answers a repeated call from the file. Answers are kept per account, so switching keys never shows one account another's data, and any write through the same app clears them, so a read after a write is fresh. Over MCP a cached result carries `_meta["slipway/cache"]` with its age. In a terminal, `--refresh` fetches again, and `<PREFIX>_CACHE=0` turns the cache off.
|
|
321
|
+
|
|
322
|
+
**Synced lists.** A list tool with `sync: { id: "id" }` can be copied, every page of it, and searched offline:
|
|
323
|
+
|
|
324
|
+
```bash
|
|
325
|
+
notes-cli data sync list-notes # copy every page
|
|
326
|
+
notes-cli data search grocery list # full-text, accents ignored
|
|
327
|
+
notes-cli data sql "select json_extract(data, '$.title') from records where tool = 'list_notes'"
|
|
328
|
+
notes-cli data # what is kept, where, for which account
|
|
329
|
+
notes-cli data clear list-notes # delete that copy
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
A sync with no filters mirrors the list, so records the service no longer lists are removed. A filtered sync only adds and updates. Over MCP, `local_sync` copies a list in the background and `local_search` searches the copy; both are in the `local` toolset.
|
|
333
|
+
|
|
334
|
+
Records are stored with registered credentials masked, `data sql` runs on a connection SQLite itself will not write through, and the file lives under `<PREFIX>_DATA_DIR` or the system's own data folder. Set `dataScope` on the app to keep data per account id rather than per key, so rotating a key keeps the copy.
|
|
335
|
+
|
|
336
|
+
## 7. OpenAPI
|
|
337
|
+
|
|
338
|
+
An API that publishes OpenAPI 3 already says what every operation takes. `fromOpenAPI()` turns each operation into a tool, and `httpExecutor()` calls it:
|
|
339
|
+
|
|
340
|
+
```ts
|
|
341
|
+
import { fromOpenAPI, httpExecutor, slipway } from "@thenavidm/slipway";
|
|
342
|
+
import spec from "./openapi.json" with { type: "json" };
|
|
343
|
+
|
|
344
|
+
export const app = slipway<{ token: string }>({
|
|
345
|
+
name: "shop",
|
|
346
|
+
version: "1.0.0",
|
|
347
|
+
context: (env) => ({ token: env.SHOP_TOKEN ?? "" }),
|
|
348
|
+
secrets: (ctx) => [ctx.token],
|
|
349
|
+
tools: fromOpenAPI(spec, {
|
|
350
|
+
execute: httpExecutor({ baseUrl: "https://api.example.com/v1", headers: (ctx) => ({ authorization: `Bearer ${ctx.token}` }) }),
|
|
351
|
+
pin: { sha256: "26620d73f4fcf9a84c6729a0d005cf973dd68a010e439df88c30f54480922739" },
|
|
352
|
+
}),
|
|
353
|
+
});
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
| From the document | Becomes |
|
|
357
|
+
|---|---|
|
|
358
|
+
| `operationId` | The tool name in snake_case. A name past 64 characters ends in a short hash, so it stays unique |
|
|
359
|
+
| The HTTP method | The risk: GET is a read, DELETE is irreversible and needs confirming, the rest are writes |
|
|
360
|
+
| Tags | Toolsets |
|
|
361
|
+
| Path, query and header parameters, and a JSON or form body | One input, with a plain body's fields spread into it. A name already taken, such as a body field called `confirm`, becomes `body_confirm` |
|
|
362
|
+
| References, `nullable`, `example`, 3.0's exclusive bounds | JSON Schema 2020-12. A schema that refers to itself is cut, and objects more than three references deep are described rather than spelled out |
|
|
363
|
+
|
|
364
|
+
Override any operation's name or risk with `names` and `risk`, keep a subset with `include`, and add `typedOutput: true` to declare documented responses as output schemas. `httpExecutor` writes each parameter in the style its document gives, maps failures to Slipway's errors with the API's own message, and refuses to send credentials over plain HTTP to another machine. A body that is a file upload or raw text is skipped, and `slipway openapi` says which and why.
|
|
365
|
+
|
|
366
|
+
`pin` refuses to build from a document that changed since someone reviewed it. `slipway openapi openapi.json` prints the hash to pin, every tool the document becomes, and any schema large enough to cost a model real context.
|
|
367
|
+
|
|
368
|
+
## 8. The CLI
|
|
369
|
+
|
|
370
|
+
| Command | What it does |
|
|
371
|
+
|---|---|
|
|
372
|
+
| `<cli>` | Every command, grouped by toolset, writes marked |
|
|
373
|
+
| `<cli> <command> [flags]` | Run one tool |
|
|
374
|
+
| `<cli> <command> --help` | Its flags, choices, defaults, examples and risk |
|
|
375
|
+
| `<cli> which <words>` | Find the command for a task, by what it does |
|
|
376
|
+
| `<cli> schema <command>` | The JSON Schema an MCP client receives. `--output` for the result's |
|
|
377
|
+
| `<cli> agent-context` | Commands, flags, risk, examples, exit codes and settings as JSON. `--brief` for names only |
|
|
378
|
+
| `<cli> doctor` | Check the setup. `--network` also calls the service |
|
|
379
|
+
| `<cli> login` | How to connect an account |
|
|
380
|
+
| `<cli> install <client>` | Add the MCP server to a client. See [Add it to a client](#10-add-it-to-a-client) |
|
|
381
|
+
| `<cli> data` | The local cache and synced lists: `sync`, `search`, `sql`, `clear` |
|
|
382
|
+
| `<cli> completion bash` | Tab completion for bash, zsh or fish |
|
|
383
|
+
|
|
384
|
+
Flags come from the schema: `--flag value`, `--flag=value`, the underscore spelling, `--no-flag` for a boolean, repeated or comma-separated lists of numbers and choices, and JSON or `@file.json` for an object. `--input` takes every argument as one JSON object, from the flag, a file or stdin, and flags on the same line override it.
|
|
385
|
+
|
|
386
|
+
| Output flag | Shape |
|
|
387
|
+
|---|---|
|
|
388
|
+
| `--json` | Pretty JSON |
|
|
389
|
+
| `--compact` | One line of JSON |
|
|
390
|
+
| `--jsonl` | One JSON value per line, for lists |
|
|
391
|
+
| `--csv`, `--tsv` | A table, for lists of records |
|
|
392
|
+
| `--quiet` | One value per line: ids, or the one `--select` field |
|
|
393
|
+
| `--select a,b.c` | Keep only these fields. Dotted paths descend into arrays |
|
|
394
|
+
| `--out <file>` | Write to a new file, readable only by you, never over an existing one |
|
|
395
|
+
| `--wait` | For a job: wait until it finishes |
|
|
396
|
+
| `--refresh` | Skip the local cache and fetch again |
|
|
397
|
+
|
|
398
|
+
| Exit code | Meaning |
|
|
399
|
+
|---|---|
|
|
400
|
+
| 0 | Ok |
|
|
401
|
+
| 1 | Unexpected error |
|
|
402
|
+
| 2 | Usage error, or a write the guard refused |
|
|
403
|
+
| 3 | Not found |
|
|
404
|
+
| 4 | Authentication or permission |
|
|
405
|
+
| 5 | Upstream API error or timeout |
|
|
406
|
+
| 7 | Rate limited |
|
|
407
|
+
| 10 | Nothing configured |
|
|
408
|
+
|
|
409
|
+
Errors are JSON on stderr, always, with `error`, `code` and a `hint` that names the fix.
|
|
410
|
+
|
|
411
|
+
## 9. The MCP server
|
|
412
|
+
|
|
413
|
+
| Run | Serves |
|
|
414
|
+
|---|---|
|
|
415
|
+
| `<mcp>` | MCP over stdio, what a client launches |
|
|
416
|
+
| `<mcp> --http [--port 8787]` | Streamable HTTP at `/mcp`, with `/health` |
|
|
417
|
+
|
|
418
|
+
HTTP binds `127.0.0.1` and checks the Host header, so a web page cannot reach it through a name that resolves to localhost. It refuses to listen on any other address without `<PREFIX>_HTTP_TOKEN`, because anyone who reached the port would act as your account.
|
|
419
|
+
|
|
420
|
+
Resources and prompts are optional and take a few lines each:
|
|
421
|
+
|
|
422
|
+
```ts
|
|
423
|
+
resources: [{ name: "guide", uri: "notes://guide", mimeType: "text/markdown", read: () => GUIDE }],
|
|
424
|
+
prompts: [{ name: "weekly-review", description: "Review this week's notes.", render: () => "Review my notes from this week." }],
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
## 10. Add it to a client
|
|
428
|
+
|
|
429
|
+
`install` adds the server to a client's own configuration, in the shape that client expects:
|
|
430
|
+
|
|
431
|
+
```bash
|
|
432
|
+
notes-cli install codex
|
|
433
|
+
notes-cli install claude-code --scope project
|
|
434
|
+
notes-cli install claude-desktop --copy-env
|
|
435
|
+
notes-cli install cursor --dry-run
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
| Client | Where it goes | How credentials reach the server |
|
|
439
|
+
|---|---|---|
|
|
440
|
+
| `claude-code` | `claude mcp add-json`, user or project scope | Claude Code passes its own environment on |
|
|
441
|
+
| `codex` | `~/.codex/config.toml`, or `.codex/config.toml` | Listed in `env_vars`, so Codex forwards them from its environment |
|
|
442
|
+
| `claude-desktop` | `claude_desktop_config.json` | Claude Desktop sees no shell environment, so you add them, or `--copy-env` copies them in and makes the file private |
|
|
443
|
+
| `cursor` | `~/.cursor/mcp.json`, or `.cursor/mcp.json` | `${env:NAME}` references |
|
|
444
|
+
| `vscode` | `.vscode/mcp.json` | VS Code asks for each credential once and stores it securely |
|
|
445
|
+
| `gemini` | `~/.gemini/settings.json`, or `.gemini/settings.json` | `${NAME}` references, which Gemini CLI needs to pass anything named like a key |
|
|
446
|
+
|
|
447
|
+
A published server is started with `npx --package=<package>@<version> <name>-mcp`, pinned to the version you installed, with Codex's startup timeout raised for the first download. Without `package`, or with `--local`, the client starts this copy on disk. Installing again updates the entry in place: anything you added to it by hand stays, and the old file is kept as a backup.
|
|
448
|
+
|
|
449
|
+
## 11. Large catalogs
|
|
450
|
+
|
|
451
|
+
A server with a hundred tools costs a client that loads every definition up front on every message. Two settings keep that down.
|
|
452
|
+
|
|
453
|
+
**Toolsets.** Tag tools, then let whoever runs the server pick: `<PREFIX>_TOOLSETS=courses,users`, or `all`. Untagged tools are always on. `defaults.toolsets` sets what is on when the variable is unset, and can be a function of the environment, which keeps an older switch like `ENABLE_BETA=1` working.
|
|
454
|
+
|
|
455
|
+
**The search surface.** `<PREFIX>_SURFACE=search` replaces the tool list with three tools: `search_tools` finds a tool by what it does, `describe_tool` returns one schema, and `call_tool` runs it through the same guard. The CLI is unaffected.
|
|
456
|
+
|
|
457
|
+
## 12. Release checks
|
|
458
|
+
|
|
459
|
+
`slipway check` runs against your built app and its real MCP server:
|
|
460
|
+
|
|
461
|
+
```bash
|
|
462
|
+
slipway check dist/app.js --bin dist/index.js --docs README.md,SKILL.md
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
| Check | What fails |
|
|
466
|
+
|---|---|
|
|
467
|
+
| Names | A tool that takes a built-in command's name |
|
|
468
|
+
| Descriptions | A description too thin to choose a tool by; arguments with no description |
|
|
469
|
+
| Schemas | Not valid JSON Schema 2020-12, a property name a client rejects, a root-level union |
|
|
470
|
+
| Size | A schema over the budget, and definitions repeated inside one schema |
|
|
471
|
+
| Examples | An example whose arguments the schema rejects |
|
|
472
|
+
| Safety | A confirmed tool with no summary for its refusal and audit line |
|
|
473
|
+
| Instructions | None, or 512 characters that never say what the server is |
|
|
474
|
+
| Parity | A tool, schema, annotation or approval flag that differs between MCP and the CLI |
|
|
475
|
+
| Docs | A command or flag in your README or SKILL.md that does not exist |
|
|
476
|
+
| Startup | A built server that exits or hangs when nothing is configured |
|
|
477
|
+
| Install | No `package`, so `install` cannot point clients at the published server |
|
|
478
|
+
|
|
479
|
+
Parity runs on both protocol revisions a client may open with. `slipway docs dist/app.js` prints the command table, every argument and the settings as Markdown, from the same definitions. `slipway inspect dist/app.js` lists the tools exactly as a client receives them, and `slipway openapi <file|url>` previews what an OpenAPI document becomes.
|
|
480
|
+
|
|
481
|
+
## 13. Testing
|
|
482
|
+
|
|
483
|
+
```ts
|
|
484
|
+
import { checkApp, cli, connect } from "@thenavidm/slipway/testing";
|
|
485
|
+
|
|
486
|
+
const mcp = await connect(app, { env: { NOTES_API_KEY: "test" } });
|
|
487
|
+
const result = await mcp.callTool("get_note", { id: 7 });
|
|
488
|
+
await mcp.close();
|
|
489
|
+
|
|
490
|
+
const { code } = await cli(app, ["delete-note", "7"], { env: {} });
|
|
491
|
+
// code is 2: refused without --confirm
|
|
492
|
+
|
|
493
|
+
const report = await checkApp(app, { env: {} });
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
`connect` talks to the real server over an in-memory transport, through the same stdio entry the binary runs. `cli` runs the real CLI with captured output. To stub the network, build the app with a context that returns a fake client.
|
|
497
|
+
|
|
498
|
+
To test approval by a person, give `connect` an `elicit` answer, and pass `era: "modern"` for the 2026-07-28 revision or `clientInfo` to be a particular client:
|
|
499
|
+
|
|
500
|
+
```ts
|
|
501
|
+
const mcp = await connect(app, { era: "modern", elicit: () => ({ action: "accept", content: { approve: true } }) });
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
## 14. Troubleshooting
|
|
505
|
+
|
|
506
|
+
| Symptom | Cause | Fix |
|
|
507
|
+
|---|---|---|
|
|
508
|
+
| A tool is missing from the list | Read-only mode hides writes, or its toolset is off | Check `<PREFIX>_READ_ONLY` and `<PREFIX>_TOOLSETS`, or run `<cli> doctor` |
|
|
509
|
+
| A destructive call keeps being refused | No confirmation on the call itself | Pass `confirm: true`, or `--confirm` in a terminal. `--agent` and `--yes` never confirm |
|
|
510
|
+
| A headless agent cannot run an irreversible tool | Nobody is there to approve it | Set `<PREFIX>_CONFIRM=model` for that run, so `confirm: true` confirms |
|
|
511
|
+
| A job tool returns `done: false` | The job outlasted the wait | Call its `_status` tool with the `job_id`, or use `--wait` in a terminal |
|
|
512
|
+
| A background job's status says not found | The server that ran it restarted, or an hour passed | Start the job again |
|
|
513
|
+
| A read returns old data | It is cached | `--refresh`, or `<PREFIX>_CACHE=0`. A write through the app clears the cache |
|
|
514
|
+
| `data` says SQLite is missing | Node.js older than 22.13 | Upgrade Node.js. Everything but local data works meanwhile |
|
|
515
|
+
| `tools/list` fails with a schema error | A schema written with Zod 3 | Use Zod 4.2 or later, imported from Slipway so the app has one copy |
|
|
516
|
+
| Exit code 10 | Nothing is configured | Run `<cli> login` and `<cli> doctor` |
|
|
517
|
+
| A client shows the server as failed | The process printed to stdout, which is the protocol channel | Log with `ctx.log`, which writes to stderr |
|
|
518
|
+
| Codex stops a long call after 60 seconds | Codex's default tool timeout | Make it a job tool, or raise `tool_timeout_sec` for the server in Codex's `config.toml` |
|
|
519
|
+
| Codex shows the server as failed at startup | The first npx download outlasted 10 seconds | `install codex` sets `startup_timeout_sec = 60`; add it by hand to an older entry |
|
|
520
|
+
| `slipway check` warns about schema size | One tool's schema is large or repeats its definitions | Send the body schema once, or advertise a short one and validate the full one in the handler |
|
|
521
|
+
| `slipway check` cannot load the app | The module starts the server when imported | Export the app from `app.ts` and call `app.main()` only in `index.ts` |
|
|
522
|
+
|
|
523
|
+
## Environment variables
|
|
524
|
+
|
|
525
|
+
Every server reads these, under its own prefix: the app name in capitals, `NOTES` for `notes`, unless `envPrefix` says otherwise. A server's own settings, declared with `settings`, are listed in its help, its `agent-context` and its generated docs.
|
|
526
|
+
|
|
527
|
+
| Variable | Default | What it does |
|
|
528
|
+
|---|---|---|
|
|
529
|
+
| `<PREFIX>_READ_ONLY` | `0` | `1` hides and refuses every write |
|
|
530
|
+
| `<PREFIX>_ALLOW_DESTRUCTIVE` | `1` | `0` keeps writes and refuses the irreversible ones |
|
|
531
|
+
| `<PREFIX>_AUDIT_LOG` | none | File that records every attempted write |
|
|
532
|
+
| `<PREFIX>_CONFIRM` | `human` | `model` lets `confirm: true` alone confirm, for an agent with no person to ask |
|
|
533
|
+
| `<PREFIX>_CACHE` | `1` | `0` never answers from the local cache |
|
|
534
|
+
| `<PREFIX>_DATA_DIR` | the system's data folder | Where the local data file lives |
|
|
535
|
+
| `<PREFIX>_TOOLSETS` | `all` | Comma-separated toolsets to turn on |
|
|
536
|
+
| `<PREFIX>_SURFACE` | `full` | `search` lists three tools that find, describe and run the rest |
|
|
537
|
+
| `<PREFIX>_TOOL_TIMEOUT_MS` | none | Give up on any tool after this long |
|
|
538
|
+
| `<PREFIX>_HTTP_PORT` | `8787` | For `--http` |
|
|
539
|
+
| `<PREFIX>_HTTP_HOST` | `127.0.0.1` | For `--http`. Any other address needs a token |
|
|
540
|
+
| `<PREFIX>_HTTP_TOKEN` | none | Bearer token required by `--http` |
|
|
541
|
+
| `<PREFIX>_DEBUG` | `0` | `1` prints debug lines on stderr |
|
|
542
|
+
|
|
543
|
+
## Versions
|
|
544
|
+
|
|
545
|
+
See [CHANGELOG.md](CHANGELOG.md).
|
|
546
|
+
|
|
547
|
+
## 15. FAQ ❓
|
|
548
|
+
|
|
549
|
+
<details>
|
|
550
|
+
<summary><b>What is an MCP server, and why ship a CLI next to it?</b></summary>
|
|
551
|
+
|
|
552
|
+
An MCP server is how an AI app such as Claude Code, Codex or Claude Desktop reaches a service: it lists tools, and the app calls them for you. A CLI reaches the same service from a terminal, a script or an agent that runs shell commands, and costs nothing until a command runs. Shipping both lets each person and each agent use the one that fits where they are.
|
|
553
|
+
|
|
554
|
+
</details>
|
|
555
|
+
|
|
556
|
+
<details>
|
|
557
|
+
<summary><b>Why would the two surfaces drift without a framework?</b></summary>
|
|
558
|
+
|
|
559
|
+
Because they are usually written twice. A flag gets added to one and not the other, a confirmation is checked in one path and forgotten in the other, an error is worded differently. Slipway generates both surfaces from one tool list and sends every call through one function, so a rule added once holds on both.
|
|
560
|
+
|
|
561
|
+
</details>
|
|
562
|
+
|
|
563
|
+
<details>
|
|
564
|
+
<summary><b>Which MCP clients does it work with?</b></summary>
|
|
565
|
+
|
|
566
|
+
Any client that speaks MCP over stdio or Streamable HTTP. Slipway uses the official MCP TypeScript SDK v2, which serves clients on the 2025 protocol and on the 2026-07-28 revision from the same server.
|
|
567
|
+
|
|
568
|
+
</details>
|
|
569
|
+
|
|
570
|
+
<details>
|
|
571
|
+
<summary><b>Does it work with Codex?</b></summary>
|
|
572
|
+
|
|
573
|
+
Yes, and `<cli> install codex` adds it. Codex launches stdio servers, reads the server's instructions, and asks before tools that are not marked read-only, so Slipway's accurate read marks matter. For an irreversible call it also shows Slipway's approval form, because Codex can be told to remember an approval and the form cannot. `slipway check` warns when the first 512 characters of the instructions never say what the server is, since that is the part Codex leans on.
|
|
574
|
+
|
|
575
|
+
</details>
|
|
576
|
+
|
|
577
|
+
<details>
|
|
578
|
+
<summary><b>Can a model confirm a destructive call by itself?</b></summary>
|
|
579
|
+
|
|
580
|
+
No, wherever the client can ask a person. Claude Code shows its own approval prompt on every call to a confirmed tool, and other clients that support elicitation show Slipway's approval form, whatever the model passed. Only a client that can do neither falls back to `confirm: true`. Approvals are signed, bound to the exact call and work once, so a client cannot invent or reuse one either.
|
|
581
|
+
|
|
582
|
+
</details>
|
|
583
|
+
|
|
584
|
+
<details>
|
|
585
|
+
<summary><b>How do I run an agent with no person watching?</b></summary>
|
|
586
|
+
|
|
587
|
+
Set `<PREFIX>_CONFIRM=model` for that run, so the model's `confirm: true` confirms and nobody is asked. Without it, Claude Code run with `-p` refuses a tool that needs a person, and Codex run with `exec` declines the approval form. Add `<PREFIX>_READ_ONLY=1` if the agent should only read.
|
|
588
|
+
|
|
589
|
+
</details>
|
|
590
|
+
|
|
591
|
+
<details>
|
|
592
|
+
<summary><b>What happens when a tool takes longer than the client waits?</b></summary>
|
|
593
|
+
|
|
594
|
+
Make it a job. The call waits up to `wait_seconds`, then returns the job with a `check` that names the status tool to call next, so the model keeps going instead of seeing a timeout. In a terminal, `--wait` waits to the end.
|
|
595
|
+
|
|
596
|
+
</details>
|
|
597
|
+
|
|
598
|
+
<details>
|
|
599
|
+
<summary><b>Is the cache safe with more than one account?</b></summary>
|
|
600
|
+
|
|
601
|
+
Yes. Cached results and synced records are stored per account, from a hash of the credentials or the app's own `dataScope`, and are only ever read back for that account. Any write clears that account's cached results, and credentials are masked before anything is stored.
|
|
602
|
+
|
|
603
|
+
</details>
|
|
604
|
+
|
|
605
|
+
<details>
|
|
606
|
+
<summary><b>Where is local data kept, and how do I delete it?</b></summary>
|
|
607
|
+
|
|
608
|
+
In one SQLite file under `<PREFIX>_DATA_DIR`, or the system's data folder: `~/Library/Application Support/slipway/<name>` on macOS, `~/.local/share/slipway/<name>` on Linux, `%LOCALAPPDATA%\slipway\<name>` on Windows. `<cli> data` shows the path, and `<cli> data clear` deletes this account's copy.
|
|
609
|
+
|
|
610
|
+
</details>
|
|
611
|
+
|
|
612
|
+
<details>
|
|
613
|
+
<summary><b>What does --agent change, and why does it never confirm?</b></summary>
|
|
614
|
+
|
|
615
|
+
It switches on JSON, one-line output, no prompts and no color, the settings an agent wants on every call. It does not confirm writes, because the agent setting that flag is the caller a confirmation exists to stop. A destructive command runs only when `--confirm` is passed on the call itself.
|
|
616
|
+
|
|
617
|
+
</details>
|
|
618
|
+
|
|
619
|
+
<details>
|
|
620
|
+
<summary><b>Does it work with tools generated from an OpenAPI document?</b></summary>
|
|
621
|
+
|
|
622
|
+
Yes. `fromOpenAPI(document, { execute })` turns every operation into a tool, with its risk, toolsets and a hash pin, and `httpExecutor()` calls the API. For one operation from a contract, wrap its JSON Schema with `jsonSchema({...})`.
|
|
623
|
+
|
|
624
|
+
</details>
|
|
625
|
+
|
|
626
|
+
<details>
|
|
627
|
+
<summary><b>How do I stop a large server from flooding a model's context?</b></summary>
|
|
628
|
+
|
|
629
|
+
Tag tools into toolsets and let `<PREFIX>_TOOLSETS` turn on only the ones a person needs, or set `<PREFIX>_SURFACE=search` to replace the list with three tools that find, describe and run the rest. `slipway check` also warns when one tool's schema is large or repeats its own definitions.
|
|
630
|
+
|
|
631
|
+
</details>
|
|
632
|
+
|
|
633
|
+
<details>
|
|
634
|
+
<summary><b>What happens when nothing is configured?</b></summary>
|
|
635
|
+
|
|
636
|
+
The server still starts and lists its tools, so a client shows them instead of a failed server. A call that needs an account fails with exit code 10 and a hint, and `doctor` says what is missing. `slipway check --bin` tests exactly this before a release.
|
|
637
|
+
|
|
638
|
+
</details>
|
|
639
|
+
|
|
640
|
+
<details>
|
|
641
|
+
<summary><b>How are credentials kept out of results?</b></summary>
|
|
642
|
+
|
|
643
|
+
An app returns its credentials from `secrets`, and Slipway masks those values in every result, error, `doctor` report and dry-run preview, on both surfaces. Any field named like a credential, such as `authorization`, `password` or `api_key`, is masked whatever its value.
|
|
644
|
+
|
|
645
|
+
</details>
|
|
646
|
+
|
|
647
|
+
<details>
|
|
648
|
+
<summary><b>Can I use Valibot or ArkType instead of Zod?</b></summary>
|
|
649
|
+
|
|
650
|
+
Yes. Any schema that implements Standard Schema with JSON Schema conversion works as `input` or `output`. Slipway re-exports Zod so an app has one copy of it.
|
|
651
|
+
|
|
652
|
+
</details>
|
|
653
|
+
|
|
654
|
+
<details>
|
|
655
|
+
<summary><b>How do I return images or files?</b></summary>
|
|
656
|
+
|
|
657
|
+
Return `content([image(bytes, "image/png")], data)`. `audio()`, `file()` and `resourceLink()` cover the other kinds. The parts go to the client as they are, the optional `data` goes out as structured content, and the CLI describes binary parts instead of printing them.
|
|
658
|
+
|
|
659
|
+
</details>
|
|
660
|
+
|
|
661
|
+
<details>
|
|
662
|
+
<summary><b>Do I need an output schema?</b></summary>
|
|
663
|
+
|
|
664
|
+
No. An object result already goes out as `structuredContent`. Declare `output` when you want the result validated before it leaves the server and its shape advertised to clients, which lets a client use the data without parsing text.
|
|
665
|
+
|
|
666
|
+
</details>
|
|
667
|
+
|
|
668
|
+
<details>
|
|
669
|
+
<summary><b>How do I test a server without calling the real API?</b></summary>
|
|
670
|
+
|
|
671
|
+
Build the app with a context that returns a fake client, then use `connect` for the MCP surface and `cli` for the terminal, both from `@thenavidm/slipway/testing`. They run the real server and the real CLI in memory, so a test covers the guard, the schemas and the exit codes along with your handler.
|
|
672
|
+
|
|
673
|
+
</details>
|
|
674
|
+
|
|
675
|
+
<details>
|
|
676
|
+
<summary><b>What does slipway check catch that unit tests miss?</b></summary>
|
|
677
|
+
|
|
678
|
+
The failures users meet first: a schema a client rejects, an example that no longer matches its tool, a README command that does not exist, a difference between what MCP clients and the CLI receive, and a built server that exits when nothing is configured. Unit tests run your handlers; `slipway check` runs what you ship.
|
|
679
|
+
|
|
680
|
+
</details>
|
|
681
|
+
|
|
682
|
+
<details>
|
|
683
|
+
<summary><b>Can I run a server over HTTP?</b></summary>
|
|
684
|
+
|
|
685
|
+
Yes, with `<mcp> --http`. It binds `127.0.0.1` by default, checks the Host header, and refuses any other address unless `<PREFIX>_HTTP_TOKEN` is set, so a server that acts with your account is never open to the network by accident.
|
|
686
|
+
|
|
687
|
+
</details>
|
|
688
|
+
|
|
689
|
+
<details>
|
|
690
|
+
<summary><b>How do I move an existing server onto Slipway?</b></summary>
|
|
691
|
+
|
|
692
|
+
Keep your tool modules and API client. Wrap each tool with `defineTool`, or `jsonSchema` for contract tools, build the app in `app.ts`, and delete the hand-written server, CLI and guard files. Two servers moved this way kept every tool name, title, description and annotation unchanged, and all of their existing tests passed.
|
|
693
|
+
|
|
694
|
+
</details>
|
|
695
|
+
|
|
696
|
+
<details>
|
|
697
|
+
<summary><b>Which Node.js versions does it support?</b></summary>
|
|
698
|
+
|
|
699
|
+
Node.js 22 and later, the oldest release line that is still maintained. Local data needs 22.13 or later, for the SQLite built into Node.js.
|
|
700
|
+
|
|
701
|
+
</details>
|
|
702
|
+
|
|
703
|
+
<details>
|
|
704
|
+
<summary><b>Does install write my API key into a config file?</b></summary>
|
|
705
|
+
|
|
706
|
+
Not unless you ask. Each client gets a reference instead: Codex forwards the variable, Cursor and Gemini CLI read it from their environment, and VS Code asks for it once and stores it securely. Claude Desktop cannot read a shell's environment, so it gets the key only with `--copy-env`, and the file is then made readable by you only.
|
|
707
|
+
|
|
708
|
+
</details>
|
|
709
|
+
|
|
710
|
+
<details>
|
|
711
|
+
<summary><b>Is it free to use?</b></summary>
|
|
712
|
+
|
|
713
|
+
Yes, under the Apache 2.0 license: use it, change it and ship servers built on it, commercial ones included.
|
|
714
|
+
|
|
715
|
+
</details>
|
|
716
|
+
|
|
717
|
+
<details>
|
|
718
|
+
<summary><b>Why is it called Slipway?</b></summary>
|
|
719
|
+
|
|
720
|
+
A slipway is the ramp a ship is built on and launched from. That is the job here: build a tool once, then launch it to every client and every terminal from the same place.
|
|
721
|
+
|
|
722
|
+
</details>
|
|
723
|
+
|
|
724
|
+
## Questions
|
|
725
|
+
|
|
726
|
+
Run into a problem or have a question? [Open an issue](https://github.com/thenavidm/slipway/issues) and I will help.
|
|
727
|
+
|
|
728
|
+
## About the author
|
|
729
|
+
|
|
730
|
+
Navid Moazzez is a leading AI business strategist, and the host of the AI Creator Summit, watched by 100,000+ creators. He helps creators and founders master AI and build their own AI Operating System (AI OS) to automate their business and life. He creates useful free tools, MCP servers and CLIs that creators and founders can use in their own workflows.
|
|
731
|
+
|
|
732
|
+
**Links**
|
|
733
|
+
|
|
734
|
+
- Personal website: [navid.me](https://navid.me?utm_source=github&utm_medium=referral&utm_campaign=slipway&utm_content=readme)
|
|
735
|
+
- Link in bio: [navid.bio](https://navid.bio)
|
|
736
|
+
- Navid Media: [navid.media](https://navid.media?utm_source=github&utm_medium=referral&utm_campaign=slipway&utm_content=readme)
|
|
737
|
+
- YouTube: [@thenavidm](https://youtube.com/@thenavidm?sub_confirmation=1) and [@thenavidai](https://youtube.com/@thenavidai?sub_confirmation=1)
|
|
738
|
+
- X: [@thenavidm](https://x.com/thenavidm)
|
|
739
|
+
- Instagram: [@thenavidm](https://instagram.com/thenavidm)
|
|
740
|
+
- LinkedIn: [thenavidm](https://linkedin.com/in/thenavidm)
|
|
741
|
+
|
|
742
|
+
## Dependencies
|
|
743
|
+
|
|
744
|
+
| Library | License | What it does |
|
|
745
|
+
|---|---|---|
|
|
746
|
+
| [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk) (`@modelcontextprotocol/server`) | Apache-2.0 | The MCP server, transports, protocol eras, elicitation and JSON Schema validation |
|
|
747
|
+
| [Zod](https://github.com/colinhacks/zod) | MIT | Tool schemas and validation |
|
|
748
|
+
| [Ajv](https://github.com/ajv-validator/ajv), optional, development only | MIT | JSON Schema 2020-12 checks in `slipway check` |
|
|
749
|
+
| [yaml](https://github.com/eemeli/yaml), optional, development only | ISC | Reading YAML documents in `slipway openapi` |
|
|
750
|
+
|
|
751
|
+
## License
|
|
752
|
+
|
|
753
|
+
[Apache 2.0](./LICENSE). Free to use, modify, and share.
|
|
754
|
+
|
|
755
|
+
---
|
|
756
|
+
|
|
757
|
+
© 2026 [Navid Media](https://navid.media?utm_source=github&utm_medium=referral&utm_campaign=slipway&utm_content=readme). Made with ❤️ by [Navid Moazzez](https://navid.me?utm_source=github&utm_medium=referral&utm_campaign=slipway&utm_content=readme).
|