Slipway
Converts GitHub's OpenAPI document into tools via fromOpenAPI(), turning 1,230 of GitHub's 1,232 API operations into MCP/CLI tools with risk levels inferred from the HTTP method, tags exposed as toolsets, and a hash pin that refuses to use a changed document.
Converts Stripe's OpenAPI document into tools via fromOpenAPI(), turning 611 of Stripe's 612 API operations into MCP/CLI tools with risk levels inferred from the HTTP method, tags exposed as toolsets, and a hash pin that refuses to use a changed document.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Slipwayinstall my notes server into Claude Code"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Slipway: MCP Server & CLI Framework
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.
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.
Built and maintained by Navid Moazzez.
Two ways to use it
Every server built on Slipway ships both, from one definition like this:
import { defineTool, slipway, z } from "@thenavidm/slipway";
const deleteNote = defineTool({
name: "delete_note",
title: "Delete a note",
description: "Delete a note forever. There is no undo and no trash.",
input: z.object({ id: z.number().int().describe("The note id.") }),
risk: "destructive",
positional: ["id"],
summary: ({ id }) => `delete note ${id}`,
handler: ({ id }, ctx) => ctx.api.deleteNote(id),
});
export const app = slipway({
name: "notes",
version: "1.0.0",
context: (env) => ({ api: new NotesApi(env.NOTES_API_KEY) }),
tools: [deleteNote],
});Command line
<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.
notes-cli # every command, writes marked
notes-cli which remove a note # find the command for a task
notes-cli get-note 7 --select title --compact
notes-cli list-notes --all --jsonl # follow every page, one record per line
notes-cli delete-note 7 # refused: irreversible, so it needs --confirm
notes-cli delete-note 7 --dry-run # what would run, without running it
notes-cli agent-context # commands, flags, risk and exit codes as JSONMCP server, for your AI app
<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.
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.
Which one
Where you are | What you can reach |
An agent that runs shell commands, like Claude Code or Codex | Both. The CLI costs nothing until a command runs |
A chat app with no shell, like claude.ai or Claude Desktop | The MCP server only |
A terminal, a script, cron or CI | The CLI only |
They are the same program reading the same tool definitions, so anything one can do, the other can.
Related MCP server: claudecode-mcp
Features
One definition, two surfaces. A tool added today is a command today, under the same name, with the same arguments.
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.
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: trueonly where a client can do neither. An approval is signed, bound to the exact call, and works once.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,
--waitwaits to the end.Local data. Reads can opt in to a cache, kept per account and cleared by any write.
data sync,data searchanddata sqlkeep an offline, searchable copy of any list, in one private SQLite file with nothing to install.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.One command to install.
<cli> install codex, orclaude-code,claude-desktop,cursor,vscodeorgemini, adds the server to that client's own configuration and passes credentials on instead of writing them down.Typed results. Declare an output schema and results also go out as validated
structuredContent. Without one, a result is compact text alone: Codex reads a structured copy in place of the text, and on a measured call that cost 211 more tokens.Contract tools. A tool built from a pinned JSON contract joins the same list as a hand-written Zod tool, with
jsonSchema({...}).Toolsets and a search surface for large catalogs, so a client loads only what a person turns on.
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.
Secrets stay out. Registered credentials, and any field named like one, are masked in every result and error.
A release gate.
slipway checktests schemas, sizes, examples, MCP and CLI parity, the commands your docs mention, and that the built server starts with nothing configured.Light. Two runtime dependencies: the official MCP SDK and Zod. Local data uses the SQLite built into Node.js.
Contents
# | Section | What it covers |
1 | Requirements and the local install | |
2 | The three files every server needs | |
3 | Every field of | |
4 | Risk levels, approval by a person, read-only mode, the audit log | |
5 | Jobs, status tools and waiting | |
6 | The cache, synced lists, offline search and SQL | |
7 | Every operation in a document as a tool, pinned | |
8 | Commands, flags, output shapes and exit codes | |
9 | Transports, HTTP security, resources and prompts | |
10 |
| |
11 | Toolsets and the search surface | |
12 |
| |
13 | In-memory MCP and CLI helpers, both protocol eras | |
14 | Symptoms, causes and fixes | |
15 | Twenty-five questions, answered |
1. Install
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.
npm install @thenavidm/slipway
npm install --save-dev ajvajv 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.
2. Build a server
A server is three files, plus a one-line fourth for npx.
src/tools.ts says what the server can do. toolkit<Context>() binds the context type once, so every handler gets ctx.api typed:
import { toolkit, z } from "@thenavidm/slipway";
import type { Context } from "./app.js";
const { defineTool } = toolkit<Context>();
export const getNote = defineTool({
name: "get_note",
title: "Get a note",
description: "Read one note by its id, with its full body.",
input: z.object({ id: z.number().int().min(1).describe("The note id.") }),
output: z.object({ id: z.number(), title: z.string(), body: z.string() }),
risk: "read",
positional: ["id"],
examples: [{ description: "Read note 7", args: { id: 7 } }],
handler: ({ id }, ctx) => ctx.api.getNote(id, { signal: ctx.signal }),
});src/app.ts describes the server and never starts it, so tests and slipway check can import it:
import { slipway } from "@thenavidm/slipway";
import { NotesApi } from "./api.js";
import { getNote } from "./tools.js";
export type Context = { api: NotesApi; key?: string };
export const app = slipway<Context>({
name: "notes",
title: "Notes",
version: "1.0.0",
instructions: "Notes: read and manage notes. delete_note needs confirm: true.",
context: (env) => ({ api: new NotesApi(env.NOTES_API_KEY), key: env.NOTES_API_KEY }),
configured: (ctx) => Boolean(ctx.key),
secrets: (ctx) => [ctx.key],
settings: [{ env: "NOTES_API_KEY", description: "A key from the notes dashboard.", secret: true }],
login: "Set NOTES_API_KEY to a key from the notes dashboard.",
tools: [getNote],
});src/index.ts is both binaries:
#!/usr/bin/env node
import * as nodeModule from "node:module";
nodeModule.enableCompileCache?.();
const { app } = await import("./app.js");
await app.main();The app loads after Node's compile cache goes on, so every launch after the first skips compiling it again: Bluesky answers a client in 183 ms instead of 204. Node before 22.8 has no compile cache and starts as before, and NODE_DISABLE_COMPILE_CACHE=1 turns it off.
src/npx.ts is what npx -y @you/notes-mcp-cli runs:
#!/usr/bin/env node
import "./index.js";{
"bin": { "notes-mcp": "dist/index.js", "notes-cli": "dist/index.js", "notes-mcp-cli": "dist/npx.js" }
}npx picks a binary named after the package only when the binaries point to different files. When they all share one file it starts whichever one the registry lists first, and the registry does not keep the order they were published in, so a client could get the CLI's command list instead of a server. The fourth binary, on its own file, is picked every time, and slipway check fails a package without it.
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.
The context is built on the first call that needs it, never at startup, and so are each tool's JSON Schema and its validator: Stripe's 611 generated tools are ready in about 70 ms. --help works with nothing configured, and the server answers a client at once and explains what is missing instead of exiting.
3. Tools
Field | What it does |
| snake_case. The MCP tool name; the CLI command is the same name with dashes |
| The CLI command, when the name with dashes is taken by one of the CLI's own, such as |
| A few words for pickers and the command list |
| What it does and when to use it. The only documentation a model reads before calling |
| A Zod object, or |
| Optional. Results are validated against it and sent as |
|
|
| Defaults to true for destructive tools. Set it on a write that spends money |
|
|
|
|
| The call spends money or credits, a paid generation: it needs confirming, |
| What a confirmed call does, in the tool's own words for the refusal and the approval form: "moves money and cannot be undone". Defaults to "is public or cannot be undone" |
| Annotation hints. Reads are idempotent by default; every tool is open world unless it never leaves the machine |
| Toolsets this tool belongs to. A tool with no tags is always on |
| One line for the refusal message and the audit log: "delete note 7" |
| What the person approving a call reads under the summary, and the audit log never keeps: the words of a private message about to be sent |
| What |
| Arguments as a client sends them. Shown as runnable commands in help and checked by |
| Inputs that may be typed as bare words, in order |
| How the tool pages, so |
| The tool starts work that outlasts a call. See Long-running jobs |
|
|
|
|
| Abort the call after this long |
| This tool's results are legitimately large; raises Claude Code's limit for it |
| Text for the result when JSON is not the best way to read it |
|
|
A handler returns plain data. An object goes out as compact JSON text, and as typed structuredContent too when the tool declares output. Return content([...], data) with image(), audio(), file() or resourceLink() for anything that is not text.
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.
A tool built from a pinned contract joins the same list. To make a tool of every operation in an API, see OpenAPI.
import { defineTool, jsonSchema } from "@thenavidm/slipway";
export const renameCourse = defineTool({
name: "rename_course",
title: "Rename a course",
description: "Change a course's name. Generated from the Admin API contract.",
input: jsonSchema<{ id: number; name: string }>({
type: "object",
properties: { id: { type: "integer", format: "int32" }, name: { type: "string" } },
required: ["id", "name"],
additionalProperties: false,
}),
risk: "write",
handler: ({ id, name }, ctx) => ctx.api.patch(`/courses/${id}`, { name }),
});A contract schema often spells one definition out everywhere it is used. jsonSchema(schema, { shareRepeats: true }) advertises it with each repeated part written once under $defs and referred to with $ref. Beehiiv's create-post tool went from 387 KB to 40 KB this way, and nothing is lost: Claude Code and Codex both read fields that appear only under $defs, validation accepts and refuses the same arguments, and the CLI's flags read through the references. slipway check says when it would help, and by how much.
4. Safety
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.
Risk | Example | Guard |
| List posts | None |
| Like a post, add a label | Off in read-only mode |
| Publish, delete, block | Needs confirming. Off in read-only mode, and off when irreversible writes are switched off |
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:
Client | Who confirms an irreversible call |
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 |
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 |
A client that can do neither | The model, with |
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.
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.
--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.
Three switches belong to whoever runs the server, under the server's own prefix:
Setting | Effect |
| Every write disappears from both surfaces, and a direct call is refused. A tool marked |
| Writes stay, irreversible ones are refused |
| Every attempted write is appended to this file, with its outcome and who confirmed it |
|
|
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.
5. Long-running jobs
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.
A job the service runs, with its own status endpoint:
export const renderVideo = defineTool({
name: "render_video",
title: "Render a video",
description: "Render a video from a script. Rendering takes several minutes.",
input: z.object({ script: z.string().describe("What the video says.") }),
risk: "write",
job: {
id: "id",
status: (id, ctx) => ctx.api.getRender(id, { signal: ctx.signal }),
done: (render) => render.state === "done" || render.state === "failed",
failed: (render) => render.state === "failed",
progress: (render) => ({ progress: render.percent, total: 100 }),
},
handler: ({ script }, ctx) => ctx.api.startRender(script),
});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.
What the caller sees | When |
| The job finished within the wait |
| It is still running. |
An error with the service's status as its details |
|
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.
notes-cli render-video --script "Hello" --wait
notes-cli render-video-status r_123 --wait6. Local data
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.
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.
Synced lists. A list tool with sync: { id: "id" } can be copied, every page of it, and searched offline:
notes-cli data sync list-notes # copy every page
notes-cli data search grocery list # full-text, accents ignored
notes-cli data sql "select json_extract(data, '$.title') from records where tool = 'list_notes'"
notes-cli data # what is kept, where, for which account
notes-cli data clear list-notes # delete that copyA 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.
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.
7. OpenAPI
An API that publishes OpenAPI 3 already says what every operation takes. fromOpenAPI() turns each operation into a tool, and httpExecutor() calls it:
import { fromOpenAPI, httpExecutor, slipway } from "@thenavidm/slipway";
import spec from "./openapi.json" with { type: "json" };
export const app = slipway<{ token: string }>({
name: "shop",
version: "1.0.0",
context: (env) => ({ token: env.SHOP_TOKEN ?? "" }),
secrets: (ctx) => [ctx.token],
tools: fromOpenAPI(spec, {
execute: httpExecutor({ baseUrl: "https://api.example.com/v1", headers: (ctx) => ({ authorization: `Bearer ${ctx.token}` }) }),
pin: { sha256: "26620d73f4fcf9a84c6729a0d005cf973dd68a010e439df88c30f54480922739" },
}),
});From the document | Becomes |
| The tool name in snake_case. A name past 64 characters ends in a short hash, so it stays unique |
The HTTP method | The risk: GET is a read, DELETE is irreversible and needs confirming, the rest are writes |
Tags | Toolsets |
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 |
References, | 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 |
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.
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.
8. The CLI
Command | What it does |
| Every command, grouped by toolset, writes marked |
| Run one tool |
| Its flags, choices, defaults, examples and risk |
| Find the command for a task, by what it does, with its help when one fits well ahead of the rest. An app's |
| The JSON Schema an MCP client receives. |
| Commands, flags, risk, examples, exit codes and settings as JSON. |
| Check the setup. |
| How to connect an account: printed steps, or the app's own sign-in flow with the words after |
| Add the MCP server to a client. See Add it to a client |
| The local cache and synced lists: |
| A terminal command the app adds, such as |
| Tab completion for bash, zsh or fish |
Flags come from the schema: --flag value, --flag=value, the underscore spelling, any name an app's flagAliases gives ({ ar: "aspect" } for --ar 16:9), --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.
Output flag | Shape |
| Pretty JSON |
| One line of JSON |
| One JSON value per line, for lists |
| A table, for lists of records |
| One value per line: ids, or the one |
| Keep only these fields. Dotted paths descend into arrays, and a field not at the top selects inside the one list a result holds, keeping the rest |
| Write to a new file, readable only by you, never over an existing one |
| For a job: wait until it finishes |
| Skip the local cache and fetch again |
Exit code | Meaning |
0 | Ok |
1 | Unexpected error |
2 | Usage error, or a write the guard refused |
3 | Not found |
4 | Authentication or permission |
5 | Upstream API error or timeout |
7 | Rate limited |
10 | Nothing configured |
Errors are JSON on stderr, always, with error, code and a hint that names the fix.
9. The MCP server
Run | Serves |
| MCP over stdio, what a client launches |
| Streamable HTTP at |
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, and it refuses a request whose Origin is another site unless <PREFIX>_HTTP_ALLOWED_ORIGINS lists it, as the MCP transport spec asks. It refuses to listen on any other address without <PREFIX>_HTTP_TOKEN, because anyone who reached the port would act as your account.
Work that belongs to a running server goes in onServe(ctx, log, session), which runs once the server is answering over either transport and never for a CLI command: a queue that publishes on time, or a warning that a token expires this week. A throw there is logged and the server keeps serving.
A Claude Code channel pushes events into a running session. Declare experimental: { "claude/channel": {} } and send each event from onServe with session.notify("notifications/claude/channel", { content, meta }); notify resolves false over HTTP, where no session is waiting. Check who sent an event before you push it, because whatever you push becomes text in front of the model.
Resources and prompts are optional and take a few lines each:
resources: [{ name: "guide", uri: "notes://guide", mimeType: "text/markdown", read: () => GUIDE }],
prompts: [{ name: "weekly-review", description: "Review this week's notes.", render: () => "Review my notes from this week." }],10. Add it to a client
install adds the server to a client's own configuration, in the shape that client expects:
notes-cli install codex
notes-cli install claude-code --scope project
notes-cli install claude-desktop --copy-env
notes-cli install cursor --dry-runClient | Where it goes | How credentials reach the server |
|
| Claude Code passes its own environment on |
|
| Listed in |
|
| Claude Desktop sees no shell environment, so you add them, or |
|
|
|
|
| VS Code asks for each credential once and stores it securely |
|
|
|
A published server is started with npx --package=<package>@latest <name>-mcp, so a client picks up every release on its next start, with Codex's startup timeout raised for the download. The binary is named, because npx alone starts whichever binary a package lists first. 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. A setting marked tuning: true, such as a timeout with a working default, stays out of the entry, so it carries only what connects an account.
11. Large catalogs
A server with a hundred tools costs a client that loads every definition up front on every message. Two settings keep that down.
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. The function also gets the surface asking, mcp or cli, so a server whose older switch only chose what an MCP client loads can leave its terminal running every command; a variable that is set applies to both. defaults.readOnly and defaults.allowDestructive take the same two arguments.
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.
12. Release checks
slipway check runs against your built app and its real MCP server:
npx slipway check dist/app.js --bin dist/index.js --docs README.md,SKILL.mdRun it in a project that has @thenavidm/slipway installed, where npx uses that copy. Anywhere else, name the package: npx -p @thenavidm/slipway slipway openapi spec.json. A bare npx slipway with nothing installed fetches an unrelated npm package of the same name.
Check | What fails |
Names | A tool that takes a built-in command's name |
Descriptions | A description too thin to choose a tool by; arguments with no description |
Schemas | Not valid JSON Schema 2020-12, a property name a client rejects, a root-level union |
Size | A schema over the budget, and definitions repeated inside one schema |
Examples | An example whose arguments the schema rejects |
Safety | A confirmed tool with no summary for its refusal and audit line |
Instructions | None, or 512 characters that never say what the server is |
Parity | A tool, schema, annotation or approval flag that differs between MCP and the CLI |
Docs | A command or flag in your README or SKILL.md that does not exist |
Startup | A built server that exits or hangs when nothing is configured |
Install | No |
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.
13. Testing
import { checkApp, cli, connect, resultData } from "@thenavidm/slipway/testing";
const mcp = await connect(app, { env: { NOTES_API_KEY: "test" } });
const note = resultData(await mcp.callTool("get_note", { id: 7 }));
await mcp.close();
const { code } = await cli(app, ["delete-note", "7"], { env: {} });
// code is 2: refused without --confirm
const report = await checkApp(app, { env: {} });connect talks to the real server over an in-memory transport, through the same stdio entry the binary runs. resultData reads what a call returned, typed or not. cli runs the real CLI with captured output. To stub the network, build the app with a context that returns a fake client.
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:
const mcp = await connect(app, { era: "modern", elicit: () => ({ action: "accept", content: { approve: true } }) });14. Troubleshooting
Symptom | Cause | Fix |
A tool is missing from the list | Read-only mode hides writes, or its toolset is off | Check |
A destructive call keeps being refused | No confirmation on the call itself | Pass |
A headless agent cannot run an irreversible tool | Nobody is there to approve it | Set |
A job tool returns | The job outlasted the wait | Call its |
A background job's status says not found | The server that ran it restarted, or an hour passed | Start the job again |
A read returns old data | It is cached |
|
| Node.js older than 22.13 | Upgrade Node.js. Everything but local data works meanwhile |
| A schema written with Zod 3 | Use Zod 4.2 or later, imported from Slipway so the app has one copy |
Exit code 10 | Nothing is configured | Run |
A client shows the server as failed | The process printed to stdout, which is the protocol channel | Log with |
Codex stops a long call after 60 seconds | Codex's default tool timeout | Make it a job tool, or raise |
Codex shows the server as failed at startup | The first npx download outlasted 10 seconds |
|
| 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 |
| The module starts the server when imported | Export the app from |
A request piped to the server gets no answer | Stdin closed before the answer, and the MCP stdio binding stops a server when its input ends | Keep stdin open until you read the answer, as clients do, or run the command from the CLI |
| Slipway is not installed in this folder, so npx fetched an unrelated package called | Run |
Environment variables
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, and install passes on every one not marked tuning.
Variable | Default | What it does |
|
|
|
|
|
|
| none | File that records every attempted write |
|
|
|
|
|
|
| the system's data folder | Where the local data file lives |
|
| Comma-separated toolsets to turn on. Listed only when some tool has a toolset |
|
|
|
| none | Give up on any tool after this long |
|
| For |
|
| For |
| none | Bearer token required by |
| none | Browser origins beyond localhost that may call |
|
|
|
Versions
See CHANGELOG.md.
Servers built on Slipway
Server | Package | Covers |
Image 5 generation and editing, video, generative fill and expansion, composites, upscaling, reference uploads, jobs and custom models | ||
The Photos library on this Mac: Apple's own on-device search, looking at photos, metadata, keywords, albums, favorites, exports of originals, and archiving with a person's approval | ||
The catalog, charts per country, reviews and feeds, your library on this Mac with its transcript excerpts, and analytics for a show you own | ||
Templates, image and animation renders, media jobs, workflows, assets, webhooks and Instant URLs, across private workspaces | ||
Publications, posts and drafts, subscribers, segments, automations, custom fields, tiers, polls, referrals and webhooks | ||
Posting, threads, replies, the timeline, search, feeds, lists, notifications and the social graph | ||
Any brand's logos, colors, fonts and company context, brand comparisons, transaction enrichment and approved logo downloads | ||
Scheduling and publishing posts across channels, ideas, content items, tags, templates and posting limits, through Buffer's GraphQL API | ||
Scheduled events and invitees, event types and availability, booking, contacts, routing forms, recaps and webhooks | ||
Spaces, posts, comments, members and access groups, courses, events, payments and refunds, through the Admin API | ||
Zones, DNS records with reviewed batches, cache purges, rulesets, page rules, Workers and analytics, across accounts | ||
Template editing, free validation, rendering and render batches, across separate projects | ||
| Links and link batches, analytics, conversions and partners, across workspaces | |
Facebook Pages through Meta's Graph API: posts, scheduling and drafts, Page and post insights, and comment moderation, read-only until writes are turned on | ||
Models, queued generation with receipts, assets and approved generation batches, across accounts | ||
Subscribers, segments and batch upserts, draft campaigns, workflows, custom fields and webhooks | ||
FluentCRM, FluentCommunity and Fluent Forms across several sites, with cross-plugin tasks and snapshots | ||
Products, collections, promotions, giveaways, gifting, digital files and media, streaming and webhooks, across shops | ||
The photo picker, uploads, albums and their captions, places and maps, and media this server uploaded, across several Google accounts | ||
Search analytics by query, page, country and device, period comparisons, queries just off page one, URL inspection, sitemaps and verification, across Google accounts | ||
Gmail, Drive, Docs, Sheets, Slides, Calendar, Tasks, Forms and Contacts through Google's own Workspace CLI, and its 400 other API methods | ||
Flows and their runs, agents, sessions, files, MCP servers, credit limits and administration | ||
Products and variants, sales and refunds, subscribers, offer codes, licenses, custom fields and payouts, across sellers | ||
Messages on this Mac: an inbox that survives restarts, search across full history, contacts, sending with a person's approval, voice note transcription and speech | ||
| Subscribers, tags, broadcasts, sequences, forms, snippets, purchases, email stats, bulk work and webhooks, through API v4 | |
Stores, products, checkouts, orders, subscriptions and their invoices, discounts, license keys and affiliates | ||
Posting, editing, threads, timelines, search, lists, notifications and following, on any instance | ||
Every ad running on Facebook, Instagram and Threads for any advertiser: copy, creatives, how long each has run, what changed, and EU spend and reach | ||
Generating images and video, following jobs, downloads, moodboards, the account's library and the explore feeds, through a signed-in browser | ||
| Open podcast analytics: downloads, unique listeners, retention, geography, app and device share, listening patterns and episode curves | |
The open podcast directory, with the transcripts and chapters it links to fetched and read, guest histories, feed health and value-for-value | ||
Public profiles, posts, transcripts, comments and ads across social platforms, each a paid call | ||
| Testimonials, imports and invites, project links and exports, across projects | |
Rendering, templates, generation models, and hosting through Serve and Ingest | ||
Drafts, publishing and scheduling, Notes, subscribers, analytics, tags, comments and researching other publications | ||
Courses, users, enrollments, pricing, coupons and transactions | ||
Your own Telegram account over MTProto: chats, history, search, sending, files, contacts, groups, topics and folders, 74 tools with 13 loaded by default, and a Claude Code channel | ||
A Space's testimonials, text and video submissions, requests by email, imports with separate consent for public use, and exports | ||
Posting, threads, carousels, replies and reply approvals, insights and keyword search | ||
Products and pricing, transactions and revenue, customers, subscriptions and affiliates, across several carts | ||
Your own account's profile and videos, ranked by any metric, posting and drafts, through TikTok's official API | ||
| Videos, bulk folder filing, showcases, chapters, captions and transcripts, comments, tags, privacy and embed presets | |
Media, folders, captions, channels, webinars, sharing, analytics and uploads, through the Data API | ||
Posts, pages, custom post types, media, terms, users and comments across several sites, plus Elementor, Rank Math, redirects and bulk edits through a helper plugin | ||
Transcripts of any public video, channel research against each channel's own median, and your own channels' videos, comments and analytics |
Each server was measured against its previous release before it moved: startup, what a client receives, CLI exit codes, and tokens in Claude Code and Codex. Its README has the numbers.
15. FAQ ❓
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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({...}).
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.
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.
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.
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.
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.
No. Without one, an object result goes out as compact JSON text, which every client reads. Declare output when you want the result validated before it leaves the server, its shape advertised, and a typed structuredContent copy sent for clients that use data without parsing text. Codex reads that copy in place of the text, so a schema is worth declaring when something uses the shape.
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.
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.
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.
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.
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.
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.
Yes, under the Apache 2.0 license: use it, change it and ship servers built on it, commercial ones included.
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.
Questions
Run into a problem or have a question? Open an issue and I will help.
About the author
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.
Links
Personal website: navid.me
Link in bio: navid.bio
Navid Media: navid.media
YouTube: @thenavidm and @thenavidai
X: @thenavidm
Instagram: @thenavidm
LinkedIn: thenavidm
Dependencies
Library | License | What it does |
MCP TypeScript SDK ( | Apache-2.0 | The MCP server, transports, protocol eras, elicitation and JSON Schema validation |
MIT | Tool schemas and validation | |
Ajv, optional, development only | MIT | JSON Schema 2020-12 checks in |
yaml, optional, development only | ISC | Reading YAML documents in |
License
Apache 2.0. Free to use, modify, and share.
© 2026 Navid Media. Made with ❤️ by Navid Moazzez.
This server cannot be deployed
Maintenance
Related MCP Connectors
MCP server for progressive tool usage at any scale (see https://klavis.ai)
One MCP endpoint for Claude, GPT & Gemini: 100+ tools + no-code connectors + agent workers.
The OpenRouter for tools. One MCP connection gives any AI agent 254 hosted tools, pay per call.
The Remote MCP server acts as a standardized bridge between LLM applications (like Claude, ChatGPT, and Cursor) and external services, enabling AI agents to access external tools and resources. Its primary capability is providing a centralized search tool to discover other MCP servers and their respective tools. Unlike local implementations, it runs remotely with OAuth authentication and permission controls for security.
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceA dual-transport MCP server that exposes your API as tools to LLM clients, supporting both stdio transport for local clients like Claude Desktop and HTTP/SSE transport for remote clients like OpenAI's Responses API.-
- AlicenseAqualityBmaintenanceLocal MCP server that wraps the headless Claude Code CLI as MCP tools, providing stateless access to Claude's coding capabilities through prompt-based interactions. It enables users to execute Claude Code commands with various prompt formats and structured outputs directly from MCP clients.3MIT
- FlicenseNot gradedqualityBmaintenanceEnables AI agents to securely discover, execute, and observe tools with role-based access control and audit logging. Serves tools over MCP stdio and HTTP for integration with Claude Desktop, Cursor, and other clients.1-
- AlicenseAqualityBmaintenanceEnables building and running zero-dependency MCP servers with automatic JSON Schema generation, exposing Python tools to Claude Desktop, Cursor, and autonomous agent fleets.2Apache 2.0