@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/dist/tool.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one definition both surfaces read.
|
|
3
|
+
*
|
|
4
|
+
* A tool is described once: its name, what it is for, the shape of its input,
|
|
5
|
+
* how risky it is and what it does. The MCP server and the CLI are both
|
|
6
|
+
* generated from that object, so a tool added today is a command today, and
|
|
7
|
+
* neither surface can describe it differently from the other.
|
|
8
|
+
*/
|
|
9
|
+
import { isBackground, MAX_WAIT_SECONDS, waitSecondsFor } from "./jobs.js";
|
|
10
|
+
import { phrase } from "./util.js";
|
|
11
|
+
import { advertised, CONTROL_NAMES, emptyInput, inputJsonSchema, isSchema, withControls, } from "./schema.js";
|
|
12
|
+
const NAME = /^[a-z][a-z0-9_]{0,63}$/;
|
|
13
|
+
const TAG = /^[a-z0-9][a-z0-9-]*$/;
|
|
14
|
+
/** Define a tool. Throws at load time on a definition that cannot work, not on the first call. */
|
|
15
|
+
export function defineTool(definition) {
|
|
16
|
+
const where = `Tool '${definition.name}'`;
|
|
17
|
+
if (!NAME.test(definition.name ?? "")) {
|
|
18
|
+
throw new Error(`${where}: names are snake_case, start with a letter and are at most 64 characters.`);
|
|
19
|
+
}
|
|
20
|
+
if (!definition.title?.trim())
|
|
21
|
+
throw new Error(`${where}: title is required.`);
|
|
22
|
+
if (!definition.description?.trim())
|
|
23
|
+
throw new Error(`${where}: description is required.`);
|
|
24
|
+
if (!["read", "write", "destructive"].includes(definition.risk)) {
|
|
25
|
+
throw new Error(`${where}: risk must be read, write or destructive.`);
|
|
26
|
+
}
|
|
27
|
+
if (definition.input !== undefined && !isSchema(definition.input)) {
|
|
28
|
+
throw new Error(`${where}: input must be a Zod object or jsonSchema({...}).`);
|
|
29
|
+
}
|
|
30
|
+
if (definition.output !== undefined && !isSchema(definition.output)) {
|
|
31
|
+
throw new Error(`${where}: output must be a Zod schema or jsonSchema({...}).`);
|
|
32
|
+
}
|
|
33
|
+
for (const tag of definition.tags ?? []) {
|
|
34
|
+
if (!TAG.test(tag))
|
|
35
|
+
throw new Error(`${where}: tag '${tag}' must be lowercase words joined by dashes.`);
|
|
36
|
+
}
|
|
37
|
+
if (typeof definition.handler !== "function")
|
|
38
|
+
throw new Error(`${where}: handler is required.`);
|
|
39
|
+
const job = definition.job;
|
|
40
|
+
if (job) {
|
|
41
|
+
if (definition.name.length > 57)
|
|
42
|
+
throw new Error(`${where}: a job tool's name is at most 57 characters, so its status tool fits in 64.`);
|
|
43
|
+
if (!isBackground(job)) {
|
|
44
|
+
const ok = (typeof job.id === "string" && job.id.length > 0) || typeof job.id === "function";
|
|
45
|
+
if (!ok || typeof job.status !== "function" || typeof job.done !== "function") {
|
|
46
|
+
throw new Error(`${where}: job needs id, status and done for a job the service runs, or { background: true }.`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (job.waitSeconds !== undefined && !(job.waitSeconds >= 0 && job.waitSeconds <= MAX_WAIT_SECONDS)) {
|
|
50
|
+
throw new Error(`${where}: job.waitSeconds must be 0-${MAX_WAIT_SECONDS}. Clients stop waiting on a call after about a minute.`);
|
|
51
|
+
}
|
|
52
|
+
if (definition.paginate)
|
|
53
|
+
throw new Error(`${where}: a job tool cannot also page.`);
|
|
54
|
+
if (definition.cache || definition.sync)
|
|
55
|
+
throw new Error(`${where}: a job tool cannot be cached or synced.`);
|
|
56
|
+
}
|
|
57
|
+
if (definition.cache) {
|
|
58
|
+
if (definition.risk !== "read")
|
|
59
|
+
throw new Error(`${where}: only reads can be cached. A write must reach the service every time.`);
|
|
60
|
+
if (!(typeof definition.cache.ttlSeconds === "number" && definition.cache.ttlSeconds > 0))
|
|
61
|
+
throw new Error(`${where}: cache.ttlSeconds must be a positive number.`);
|
|
62
|
+
}
|
|
63
|
+
let sync;
|
|
64
|
+
if (definition.sync) {
|
|
65
|
+
if (definition.risk !== "read")
|
|
66
|
+
throw new Error(`${where}: only reads can be synced.`);
|
|
67
|
+
const items = definition.sync.items ?? definition.paginate?.items;
|
|
68
|
+
if (!definition.sync.id || !items)
|
|
69
|
+
throw new Error(`${where}: sync needs id, and items unless the tool pages.`);
|
|
70
|
+
sync = { id: definition.sync.id, items };
|
|
71
|
+
}
|
|
72
|
+
const input = advertised((definition.input ?? emptyInput()));
|
|
73
|
+
const own = Object.keys(inputJsonSchema(input).properties ?? {});
|
|
74
|
+
for (const name of CONTROL_NAMES) {
|
|
75
|
+
if (own.includes(name))
|
|
76
|
+
throw new Error(`${where}: '${name}' is Slipway's own argument. Rename the input property.`);
|
|
77
|
+
}
|
|
78
|
+
const requireConfirm = definition.requireConfirm ?? definition.risk === "destructive";
|
|
79
|
+
const schema = withControls(input, {
|
|
80
|
+
confirm: requireConfirm,
|
|
81
|
+
...(job ? { wait: { defaultSeconds: waitSecondsFor(job), maxSeconds: MAX_WAIT_SECONDS } } : {}),
|
|
82
|
+
});
|
|
83
|
+
const jsonSchema = inputJsonSchema(schema);
|
|
84
|
+
const properties = Object.keys(jsonSchema.properties ?? {});
|
|
85
|
+
for (const name of definition.positional ?? []) {
|
|
86
|
+
if (!properties.includes(name))
|
|
87
|
+
throw new Error(`${where}: positional '${name}' is not an input property.`);
|
|
88
|
+
}
|
|
89
|
+
if (definition.paginate && !properties.includes(definition.paginate.cursorArg)) {
|
|
90
|
+
throw new Error(`${where}: paginate.cursorArg '${definition.paginate.cursorArg}' is not an input property.`);
|
|
91
|
+
}
|
|
92
|
+
return Object.freeze({
|
|
93
|
+
kind: "slipway.tool",
|
|
94
|
+
name: definition.name,
|
|
95
|
+
command: definition.name.replace(/_/g, "-"),
|
|
96
|
+
title: definition.title.trim(),
|
|
97
|
+
description: definition.description.trim(),
|
|
98
|
+
risk: definition.risk,
|
|
99
|
+
idempotent: definition.idempotent ?? definition.risk === "read",
|
|
100
|
+
openWorld: definition.openWorld ?? true,
|
|
101
|
+
requireConfirm,
|
|
102
|
+
tags: Object.freeze([...(definition.tags ?? [])]),
|
|
103
|
+
examples: Object.freeze([...(definition.examples ?? [])]),
|
|
104
|
+
positional: Object.freeze([...(definition.positional ?? [])]),
|
|
105
|
+
timeoutMs: definition.timeoutMs,
|
|
106
|
+
maxResultChars: definition.maxResultChars,
|
|
107
|
+
icons: definition.icons,
|
|
108
|
+
meta: definition.meta,
|
|
109
|
+
paginate: definition.paginate,
|
|
110
|
+
...(job ? { job } : {}),
|
|
111
|
+
...(definition.cache ? { cache: { ttlSeconds: definition.cache.ttlSeconds } } : {}),
|
|
112
|
+
...(sync ? { sync } : {}),
|
|
113
|
+
input,
|
|
114
|
+
schema,
|
|
115
|
+
output: definition.output ? advertised(definition.output) : undefined,
|
|
116
|
+
summary: definition.summary,
|
|
117
|
+
preview: definition.preview,
|
|
118
|
+
render: definition.render,
|
|
119
|
+
handler: definition.handler,
|
|
120
|
+
jsonSchema,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Bind `defineTool` to an app's context type once, so every handler in the
|
|
125
|
+
* repo gets `ctx.client` typed without repeating the generic:
|
|
126
|
+
*
|
|
127
|
+
* export const { defineTool } = toolkit<Context>();
|
|
128
|
+
*/
|
|
129
|
+
export function toolkit() {
|
|
130
|
+
return {
|
|
131
|
+
defineTool: (definition) => defineTool(definition),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
export function isTool(value) {
|
|
135
|
+
return value?.kind === "slipway.tool";
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* One line saying what a call is about to do, for refusals, approval forms and
|
|
139
|
+
* the audit log. The tool's own `summary`, or its title as a phrase: "delete a
|
|
140
|
+
* note". A summary that throws falls back too, because it runs before the
|
|
141
|
+
* arguments have been anywhere near the handler.
|
|
142
|
+
*/
|
|
143
|
+
export function summarize(tool, args) {
|
|
144
|
+
const fallback = phrase(tool.title);
|
|
145
|
+
try {
|
|
146
|
+
return tool.summary?.(args)?.trim() || fallback;
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return fallback;
|
|
150
|
+
}
|
|
151
|
+
}
|
package/dist/util.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small helpers more than one module needs.
|
|
3
|
+
*/
|
|
4
|
+
/** JSON with object keys sorted at every depth, so equal values always serialize the same. */
|
|
5
|
+
export declare function stableJson(value: unknown): string;
|
|
6
|
+
export declare function sha256(text: string): string;
|
|
7
|
+
/** "2.1.286" as numbers, ignoring anything after the first non-numeric part. */
|
|
8
|
+
export declare function versionParts(version: string | undefined): number[];
|
|
9
|
+
/** Whether `version` is at least `minimum`. An unreadable version is never at least anything. */
|
|
10
|
+
export declare function versionAtLeast(version: string | undefined, minimum: readonly number[]): boolean;
|
|
11
|
+
/** A title or summary as it reads mid-sentence: "Delete a course" becomes "delete a course", "GitHub issue" keeps its capitals. */
|
|
12
|
+
export declare function phrase(text: string): string;
|
package/dist/util.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small helpers more than one module needs.
|
|
3
|
+
*/
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
/** JSON with object keys sorted at every depth, so equal values always serialize the same. */
|
|
6
|
+
export function stableJson(value) {
|
|
7
|
+
return JSON.stringify(value, (_key, inner) => inner && typeof inner === "object" && !Array.isArray(inner)
|
|
8
|
+
? Object.fromEntries(Object.entries(inner).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)))
|
|
9
|
+
: inner) ?? "null";
|
|
10
|
+
}
|
|
11
|
+
export function sha256(text) {
|
|
12
|
+
return createHash("sha256").update(text).digest("hex");
|
|
13
|
+
}
|
|
14
|
+
/** "2.1.286" as numbers, ignoring anything after the first non-numeric part. */
|
|
15
|
+
export function versionParts(version) {
|
|
16
|
+
const match = /^v?(\d+(?:\.\d+)*)/.exec(version?.trim() ?? "");
|
|
17
|
+
return match ? match[1].split(".").map(Number) : [];
|
|
18
|
+
}
|
|
19
|
+
/** Whether `version` is at least `minimum`. An unreadable version is never at least anything. */
|
|
20
|
+
export function versionAtLeast(version, minimum) {
|
|
21
|
+
const parts = versionParts(version);
|
|
22
|
+
if (parts.length === 0)
|
|
23
|
+
return false;
|
|
24
|
+
for (let i = 0; i < minimum.length; i++) {
|
|
25
|
+
const have = parts[i] ?? 0;
|
|
26
|
+
if (have !== minimum[i])
|
|
27
|
+
return have > minimum[i];
|
|
28
|
+
}
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
/** A title or summary as it reads mid-sentence: "Delete a course" becomes "delete a course", "GitHub issue" keeps its capitals. */
|
|
32
|
+
export function phrase(text) {
|
|
33
|
+
const trimmed = text.trim().replace(/[.!?]+$/, "");
|
|
34
|
+
return /^[A-Z][a-z]/.test(trimmed) && !/^[A-Z][a-z]+[A-Z]/.test(trimmed) ? trimmed.charAt(0).toLowerCase() + trimmed.slice(1) : trimmed;
|
|
35
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@thenavidm/slipway",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Slipway, the TypeScript framework for MCP servers and agent-native CLIs. One tool definition ships an MCP server and a CLI, with write safety, typed results and release checks built in.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"author": "Navid Moazzez (https://navid.me)",
|
|
8
|
+
"homepage": "https://github.com/thenavidm/slipway#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/thenavidm/slipway.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/thenavidm/slipway/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"mcp",
|
|
18
|
+
"mcp-server",
|
|
19
|
+
"model-context-protocol",
|
|
20
|
+
"mcp framework",
|
|
21
|
+
"cli",
|
|
22
|
+
"cli framework",
|
|
23
|
+
"agent-native cli",
|
|
24
|
+
"ai agents",
|
|
25
|
+
"claude code",
|
|
26
|
+
"codex",
|
|
27
|
+
"typescript",
|
|
28
|
+
"tool calling",
|
|
29
|
+
"mcp sdk",
|
|
30
|
+
"standard schema",
|
|
31
|
+
"zod",
|
|
32
|
+
"openapi",
|
|
33
|
+
"llm tools",
|
|
34
|
+
"claude desktop",
|
|
35
|
+
"cursor"
|
|
36
|
+
],
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=22"
|
|
39
|
+
},
|
|
40
|
+
"exports": {
|
|
41
|
+
".": {
|
|
42
|
+
"types": "./dist/index.d.ts",
|
|
43
|
+
"import": "./dist/index.js"
|
|
44
|
+
},
|
|
45
|
+
"./testing": {
|
|
46
|
+
"types": "./dist/testing.d.ts",
|
|
47
|
+
"import": "./dist/testing.js"
|
|
48
|
+
},
|
|
49
|
+
"./package.json": "./package.json"
|
|
50
|
+
},
|
|
51
|
+
"bin": {
|
|
52
|
+
"slipway": "dist/bin.js"
|
|
53
|
+
},
|
|
54
|
+
"files": [
|
|
55
|
+
"dist",
|
|
56
|
+
"README.md",
|
|
57
|
+
"SKILL.md",
|
|
58
|
+
"LICENSE",
|
|
59
|
+
"NOTICE",
|
|
60
|
+
"CHANGELOG.md",
|
|
61
|
+
"SECURITY.md"
|
|
62
|
+
],
|
|
63
|
+
"scripts": {
|
|
64
|
+
"build": "tsc",
|
|
65
|
+
"typecheck": "tsc --noEmit",
|
|
66
|
+
"test": "vitest run",
|
|
67
|
+
"prepublishOnly": "npm run build && npm test"
|
|
68
|
+
},
|
|
69
|
+
"dependencies": {
|
|
70
|
+
"@modelcontextprotocol/server": "^2.3.0",
|
|
71
|
+
"zod": "^4.2.0"
|
|
72
|
+
},
|
|
73
|
+
"peerDependencies": {
|
|
74
|
+
"ajv": "^8.17.1"
|
|
75
|
+
},
|
|
76
|
+
"peerDependenciesMeta": {
|
|
77
|
+
"ajv": {
|
|
78
|
+
"optional": true
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
"devDependencies": {
|
|
82
|
+
"@modelcontextprotocol/client": "^2.3.0",
|
|
83
|
+
"@types/node": "^22.10.0",
|
|
84
|
+
"ajv": "^8.17.1",
|
|
85
|
+
"typescript": "^7.0.2",
|
|
86
|
+
"vite": "^8.3.2",
|
|
87
|
+
"vitest": "^5.0.3"
|
|
88
|
+
}
|
|
89
|
+
}
|