@happyvertical/smrt-app-cli 0.30.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/AGENTS.md +22 -0
- package/CLAUDE.md +1 -0
- package/LICENSE +7 -0
- package/dist/bin/smrt-mcp-bridge.d.ts +24 -0
- package/dist/bin/smrt-mcp-bridge.js +29 -0
- package/dist/config-Bgq_EQoJ.js +206 -0
- package/dist/index.d.ts +369 -0
- package/dist/index.js +1024 -0
- package/dist/smrt-mcp-bridge.d.ts +1 -0
- package/package.json +63 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# @happyvertical/smrt-app-cli
|
|
2
|
+
|
|
3
|
+
Application CLI support for SMRT-based apps.
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
- Provides reusable app-facing CLI commands, currently centered on authenticated device-code login flows.
|
|
8
|
+
- Stores app CLI configuration in a namespaced local config file selected by the consuming app context.
|
|
9
|
+
- Intended for downstream apps that need a branded CLI without reimplementing auth polling, config persistence, and server URL handling.
|
|
10
|
+
|
|
11
|
+
## Patterns
|
|
12
|
+
|
|
13
|
+
- Keep commands app-agnostic; pass app identity through `CliConfigContext`.
|
|
14
|
+
- Treat transient auth polling failures as recoverable until the server-issued expiration window closes.
|
|
15
|
+
- Do not persist tokens, server URLs, or app slugs outside the configured local CLI config path.
|
|
16
|
+
- Keep command output stream-injectable so tests can assert behavior without writing to the real terminal.
|
|
17
|
+
|
|
18
|
+
## Gotchas
|
|
19
|
+
|
|
20
|
+
- This package depends on `@happyvertical/smrt-users` for the server-side auth contract but should not import app-specific user models.
|
|
21
|
+
- Device-code login polling should distinguish pending approval, transient server/network failures, expiry, and hard auth rejection.
|
|
22
|
+
- Tests should isolate config paths with package-specific environment prefixes.
|
package/CLAUDE.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|
package/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright <2025> <Happy Vertical Corporation>
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `smrt-mcp-bridge` — generic stdio MCP bridge.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
*
|
|
6
|
+
* ```
|
|
7
|
+
* smrt-mcp-bridge --env-prefix=WILLGRIFFIN \
|
|
8
|
+
* --name=willgriffin-mcp --version=0.1.0
|
|
9
|
+
* ```
|
|
10
|
+
*
|
|
11
|
+
* All options can also be passed as env vars:
|
|
12
|
+
*
|
|
13
|
+
* - `SMRT_MCP_ENV_PREFIX` — env-prefix the bridge uses to look up the app's
|
|
14
|
+
* server URL/token/config file.
|
|
15
|
+
* - `SMRT_MCP_APP_SLUG` — directory name under `~/.config`.
|
|
16
|
+
* - `SMRT_MCP_SERVER_NAME` / `SMRT_MCP_SERVER_VERSION` — local server identity.
|
|
17
|
+
* - `SMRT_MCP_DEFAULT_SERVER_URL` — fallback server URL.
|
|
18
|
+
*
|
|
19
|
+
* Apps that want their own branded bin should call `runMcpStdioBridge`
|
|
20
|
+
* directly from `@happyvertical/smrt-app-mcp/cli` instead of going through
|
|
21
|
+
* this generic entrypoint.
|
|
22
|
+
*/
|
|
23
|
+
export {};
|
|
24
|
+
//# sourceMappingURL=smrt-mcp-bridge.d.ts.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { d as runMcpStdioBridge } from "../config-Bgq_EQoJ.js";
|
|
3
|
+
function readArg(name) {
|
|
4
|
+
const prefix = `--${name}=`;
|
|
5
|
+
const match = process.argv.find((arg) => arg.startsWith(prefix));
|
|
6
|
+
if (match) return match.slice(prefix.length);
|
|
7
|
+
const flagIdx = process.argv.indexOf(`--${name}`);
|
|
8
|
+
if (flagIdx >= 0 && flagIdx + 1 < process.argv.length) {
|
|
9
|
+
return process.argv[flagIdx + 1];
|
|
10
|
+
}
|
|
11
|
+
return void 0;
|
|
12
|
+
}
|
|
13
|
+
const envPrefix = readArg("env-prefix") ?? process.env.SMRT_MCP_ENV_PREFIX ?? "";
|
|
14
|
+
if (!envPrefix) {
|
|
15
|
+
console.error(
|
|
16
|
+
"smrt-mcp-bridge: --env-prefix=<PREFIX> (or SMRT_MCP_ENV_PREFIX) is required."
|
|
17
|
+
);
|
|
18
|
+
process.exit(2);
|
|
19
|
+
}
|
|
20
|
+
const appSlug = readArg("app-slug") ?? process.env.SMRT_MCP_APP_SLUG;
|
|
21
|
+
const defaultServerUrl = readArg("default-server-url") ?? process.env.SMRT_MCP_DEFAULT_SERVER_URL;
|
|
22
|
+
const serverName = readArg("name") ?? process.env.SMRT_MCP_SERVER_NAME ?? "smrt-app-mcp";
|
|
23
|
+
const serverVersion = readArg("version") ?? process.env.SMRT_MCP_SERVER_VERSION ?? "0.0.0";
|
|
24
|
+
await runMcpStdioBridge({
|
|
25
|
+
envPrefix,
|
|
26
|
+
appSlug,
|
|
27
|
+
defaultServerUrl,
|
|
28
|
+
serverInfo: { name: serverName, version: serverVersion }
|
|
29
|
+
});
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
+
import { randomBytes } from "node:crypto";
|
|
5
|
+
import { readFile, mkdir, chmod, writeFile, rename, unlink } from "node:fs/promises";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { join, dirname } from "node:path";
|
|
8
|
+
function createMcpStdioBridge(options) {
|
|
9
|
+
const toolsPath = options.toolsPath ?? "/api/mcp/tools";
|
|
10
|
+
const callPath = options.callPath ?? "/api/mcp/call";
|
|
11
|
+
const server = new Server(options.serverInfo, {
|
|
12
|
+
capabilities: { tools: {} }
|
|
13
|
+
});
|
|
14
|
+
server.setRequestHandler(
|
|
15
|
+
ListToolsRequestSchema,
|
|
16
|
+
async (_request) => {
|
|
17
|
+
return requestJson(
|
|
18
|
+
options,
|
|
19
|
+
toolsPath,
|
|
20
|
+
{ method: "GET" },
|
|
21
|
+
{ fetch: options.fetch }
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
);
|
|
25
|
+
server.setRequestHandler(
|
|
26
|
+
CallToolRequestSchema,
|
|
27
|
+
async (request) => {
|
|
28
|
+
const { name, arguments: args = {} } = request.params;
|
|
29
|
+
try {
|
|
30
|
+
return await requestJson(
|
|
31
|
+
options,
|
|
32
|
+
callPath,
|
|
33
|
+
{
|
|
34
|
+
body: JSON.stringify({ arguments: args, name }),
|
|
35
|
+
method: "POST"
|
|
36
|
+
},
|
|
37
|
+
{ fetch: options.fetch }
|
|
38
|
+
);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
return {
|
|
41
|
+
content: [
|
|
42
|
+
{
|
|
43
|
+
text: error instanceof Error ? error.message : "MCP tool call failed.",
|
|
44
|
+
type: "text"
|
|
45
|
+
}
|
|
46
|
+
],
|
|
47
|
+
isError: true
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
);
|
|
52
|
+
return {
|
|
53
|
+
server,
|
|
54
|
+
connect: () => server.connect(new StdioServerTransport())
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
async function runMcpStdioBridge(options) {
|
|
58
|
+
const { connect } = createMcpStdioBridge(options);
|
|
59
|
+
await connect();
|
|
60
|
+
}
|
|
61
|
+
const bridge = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
62
|
+
__proto__: null,
|
|
63
|
+
createMcpStdioBridge,
|
|
64
|
+
runMcpStdioBridge
|
|
65
|
+
}, Symbol.toStringTag, { value: "Module" }));
|
|
66
|
+
const DEFAULT_LOCAL_SERVER = "http://localhost:5173";
|
|
67
|
+
function configFilePath(context) {
|
|
68
|
+
const override = process.env[`${context.envPrefix}_CLI_CONFIG`];
|
|
69
|
+
if (override) return override;
|
|
70
|
+
const slug = context.appSlug ?? context.envPrefix.toLowerCase();
|
|
71
|
+
return join(homedir(), ".config", slug, "config.json");
|
|
72
|
+
}
|
|
73
|
+
async function loadCliConfig(context) {
|
|
74
|
+
try {
|
|
75
|
+
const raw = await readFile(configFilePath(context), "utf8");
|
|
76
|
+
if (!raw.trim()) return {};
|
|
77
|
+
return JSON.parse(raw);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
80
|
+
return {};
|
|
81
|
+
}
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async function saveCliConfig(context, config) {
|
|
86
|
+
const path = configFilePath(context);
|
|
87
|
+
const dir = dirname(path);
|
|
88
|
+
await mkdir(dir, { recursive: true, mode: 448 });
|
|
89
|
+
await chmod(dir, 448).catch(() => void 0);
|
|
90
|
+
const tmp = `${path}.${randomBytes(6).toString("hex")}.tmp`;
|
|
91
|
+
try {
|
|
92
|
+
await writeFile(tmp, `${JSON.stringify(config, null, 2)}
|
|
93
|
+
`, {
|
|
94
|
+
mode: 384
|
|
95
|
+
});
|
|
96
|
+
await chmod(tmp, 384);
|
|
97
|
+
await rename(tmp, path);
|
|
98
|
+
} catch (err) {
|
|
99
|
+
await unlink(tmp).catch(() => void 0);
|
|
100
|
+
throw err;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async function getServerUrl(context, config) {
|
|
104
|
+
const resolved = config ?? await loadCliConfig(context);
|
|
105
|
+
const url = process.env[`${context.envPrefix}_SERVER_URL`] ?? resolved.serverUrl ?? context.defaultServerUrl ?? DEFAULT_LOCAL_SERVER;
|
|
106
|
+
return url.replace(/\/+$/u, "");
|
|
107
|
+
}
|
|
108
|
+
async function getStoredToken(context, config) {
|
|
109
|
+
const resolved = config ?? await loadCliConfig(context);
|
|
110
|
+
return process.env[`${context.envPrefix}_TOKEN`] ?? resolved.token;
|
|
111
|
+
}
|
|
112
|
+
async function clearStoredToken(context) {
|
|
113
|
+
const config = await loadCliConfig(context);
|
|
114
|
+
delete config.token;
|
|
115
|
+
await saveCliConfig(context, config);
|
|
116
|
+
}
|
|
117
|
+
async function saveAuth(context, serverUrl, token) {
|
|
118
|
+
const config = await loadCliConfig(context);
|
|
119
|
+
await saveCliConfig(context, {
|
|
120
|
+
...config,
|
|
121
|
+
serverUrl: serverUrl.replace(/\/+$/u, ""),
|
|
122
|
+
token
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
const DEFAULT_MAX_RESPONSE_BYTES = 10 * 1024 * 1024;
|
|
126
|
+
async function requestJson(context, path, init = {}, options = {}) {
|
|
127
|
+
const config = options.loadedConfig ?? await loadCliConfig(context);
|
|
128
|
+
const serverUrl = (options.serverUrl ?? await getServerUrl(context, config)).replace(/\/+$/u, "");
|
|
129
|
+
const token = await getStoredToken(context, config);
|
|
130
|
+
const headers = new Headers(init.headers);
|
|
131
|
+
if (options.requireAuth && options.auth !== false && !token) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
`Not authenticated. Run \`${context.envPrefix.toLowerCase()} auth login\` first.`
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
if (!headers.has("content-type") && init.body) {
|
|
137
|
+
headers.set("content-type", "application/json");
|
|
138
|
+
}
|
|
139
|
+
if (options.auth !== false && token) {
|
|
140
|
+
headers.set("authorization", `Bearer ${token}`);
|
|
141
|
+
}
|
|
142
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
143
|
+
const response = await fetchImpl(`${serverUrl}${path}`, {
|
|
144
|
+
...init,
|
|
145
|
+
headers
|
|
146
|
+
});
|
|
147
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
148
|
+
const maxBytes = options.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES;
|
|
149
|
+
const text = await readBodyWithCap(response, maxBytes);
|
|
150
|
+
const parsed = contentType.includes("application/json") && text ? JSON.parse(text) : text;
|
|
151
|
+
if (!response.ok) {
|
|
152
|
+
const message = parsed && typeof parsed === "object" && "error" in parsed ? String(parsed.error) : `HTTP ${response.status}: ${response.statusText}`;
|
|
153
|
+
throw Object.assign(new Error(message), { status: response.status });
|
|
154
|
+
}
|
|
155
|
+
return parsed;
|
|
156
|
+
}
|
|
157
|
+
async function readBodyWithCap(response, maxBytes) {
|
|
158
|
+
if (!response.body) return "";
|
|
159
|
+
const cl = Number(response.headers.get("content-length") ?? "");
|
|
160
|
+
if (Number.isFinite(cl) && cl > maxBytes) {
|
|
161
|
+
try {
|
|
162
|
+
await response.body.cancel();
|
|
163
|
+
} catch {
|
|
164
|
+
}
|
|
165
|
+
throw new Error(
|
|
166
|
+
`Response too large: ${cl} bytes exceeds ${maxBytes}-byte cap`
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
const reader = response.body.getReader();
|
|
170
|
+
const chunks = [];
|
|
171
|
+
let size = 0;
|
|
172
|
+
try {
|
|
173
|
+
while (true) {
|
|
174
|
+
const { done, value } = await reader.read();
|
|
175
|
+
if (done) break;
|
|
176
|
+
if (!value) continue;
|
|
177
|
+
size += value.byteLength;
|
|
178
|
+
if (size > maxBytes) {
|
|
179
|
+
throw new Error(
|
|
180
|
+
`Response too large: exceeded ${maxBytes}-byte cap mid-stream`
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
chunks.push(value);
|
|
184
|
+
}
|
|
185
|
+
} finally {
|
|
186
|
+
try {
|
|
187
|
+
reader.releaseLock();
|
|
188
|
+
} catch {
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return new TextDecoder().decode(
|
|
192
|
+
Buffer.concat(chunks.map((c) => Buffer.from(c)))
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
export {
|
|
196
|
+
getStoredToken as a,
|
|
197
|
+
createMcpStdioBridge as b,
|
|
198
|
+
clearStoredToken as c,
|
|
199
|
+
runMcpStdioBridge as d,
|
|
200
|
+
saveCliConfig as e,
|
|
201
|
+
bridge as f,
|
|
202
|
+
getServerUrl as g,
|
|
203
|
+
loadCliConfig as l,
|
|
204
|
+
requestJson as r,
|
|
205
|
+
saveAuth as s
|
|
206
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import { Writable } from 'node:stream';
|
|
3
|
+
|
|
4
|
+
declare type ApiHttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
5
|
+
|
|
6
|
+
export declare interface AppCli {
|
|
7
|
+
run(argv: string[]): Promise<void>;
|
|
8
|
+
/**
|
|
9
|
+
* Wire up the stdio MCP bridge and connect to stdin/stdout. The CLI
|
|
10
|
+
* advertises itself to local MCP clients as `serverInfo.name` /
|
|
11
|
+
* `.version`. Both fields default from `options.name` (so a brand-
|
|
12
|
+
* new app gets `{ name: '<name>-mcp', version: '0.0.0' }`) — pass an
|
|
13
|
+
* explicit object to override.
|
|
14
|
+
*/
|
|
15
|
+
startMcpBridge(serverInfo?: {
|
|
16
|
+
name?: string;
|
|
17
|
+
version?: string;
|
|
18
|
+
}): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export declare interface AppCliCommand {
|
|
22
|
+
name: string;
|
|
23
|
+
description: string;
|
|
24
|
+
/**
|
|
25
|
+
* When true, the CLI ensures resources are fetched and available via
|
|
26
|
+
* `ctx.getResources()` before `run()` is called. Default `false`.
|
|
27
|
+
*/
|
|
28
|
+
needsResources?: boolean;
|
|
29
|
+
run(args: string[], ctx: AppCliContext): Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export declare interface AppCliContext {
|
|
33
|
+
config: CliConfig;
|
|
34
|
+
serverUrl: string;
|
|
35
|
+
token?: string;
|
|
36
|
+
/** Lazy resource-list fetch. Cached for the duration of the CLI invocation. */
|
|
37
|
+
getResources(): Promise<ResourceListResponse>;
|
|
38
|
+
/** Issue a JSON request via the shared client. */
|
|
39
|
+
requestJson<T = unknown>(path: string, init?: RequestInit, options?: RequestJsonOptions): Promise<T>;
|
|
40
|
+
/** Issue a raw fetch (for streaming / content-type-aware responses). */
|
|
41
|
+
request(path: string, init?: RequestInit): Promise<Response>;
|
|
42
|
+
stdout: NodeJS.WriteStream;
|
|
43
|
+
stderr: NodeJS.WriteStream;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export declare function buildFlagParser(schema: Record<string, unknown> | undefined, options?: ParserOptions): BuildParserResult;
|
|
47
|
+
|
|
48
|
+
export declare interface BuildParserResult {
|
|
49
|
+
/** Parses argv tail (everything after `<cli> <slug> <cmd> [id]`). */
|
|
50
|
+
parse(argv: string[], httpMethod: string): ParsedArgs;
|
|
51
|
+
/** Status of the underlying schema. */
|
|
52
|
+
status: SchemaSupportStatus;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Build only the URL — exposed for tests.
|
|
57
|
+
*/
|
|
58
|
+
export declare function buildUrl(context: CliConfigContext, resource: CliResource, command: CommandDefinition, parsed: ParsedArgs, id?: string): Promise<string>;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Classify a JSONSchema. The CLI uses this to decide between rich flag
|
|
62
|
+
* parsing and the positional JSON escape hatch.
|
|
63
|
+
*/
|
|
64
|
+
export declare function classifySchema(schema: Record<string, unknown> | undefined): SchemaSupportStatus;
|
|
65
|
+
|
|
66
|
+
/** Remove the token from the config file (e.g. on logout). */
|
|
67
|
+
export declare function clearStoredToken(context: CliConfigContext): Promise<void>;
|
|
68
|
+
|
|
69
|
+
/** Shape stored on disk in the app's CLI config file. */
|
|
70
|
+
export declare interface CliConfig {
|
|
71
|
+
serverUrl?: string;
|
|
72
|
+
token?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Configuration for the env-var/config-file resolution chain.
|
|
77
|
+
*
|
|
78
|
+
* Env vars derived from `envPrefix`:
|
|
79
|
+
*
|
|
80
|
+
* - `${PREFIX}_SERVER_URL`
|
|
81
|
+
* - `${PREFIX}_TOKEN`
|
|
82
|
+
* - `${PREFIX}_CLI_CONFIG` (overrides config file path)
|
|
83
|
+
*/
|
|
84
|
+
export declare interface CliConfigContext {
|
|
85
|
+
/**
|
|
86
|
+
* Uppercase prefix for env vars and the app's namespaced config dir.
|
|
87
|
+
* Example: `WILLGRIFFIN` → reads `WILLGRIFFIN_SERVER_URL`, defaults the
|
|
88
|
+
* config file to `~/.config/willgriffin/config.json` unless `appSlug`
|
|
89
|
+
* overrides the directory name.
|
|
90
|
+
*/
|
|
91
|
+
envPrefix: string;
|
|
92
|
+
/** Directory name under `~/.config`. Defaults to lowercased `envPrefix`. */
|
|
93
|
+
appSlug?: string;
|
|
94
|
+
/** Fallback server URL if neither env nor config sets one. */
|
|
95
|
+
defaultServerUrl?: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export declare type CliResource = CliResource_2;
|
|
99
|
+
|
|
100
|
+
declare interface CliResource_2 {
|
|
101
|
+
/** Kebab-case identifier; the first positional argument after the CLI name. */
|
|
102
|
+
slug: string;
|
|
103
|
+
className: string;
|
|
104
|
+
qualifiedName?: string;
|
|
105
|
+
packageName?: string;
|
|
106
|
+
label: string;
|
|
107
|
+
/** Collection segment, no leading slash, no `/api` prefix. */
|
|
108
|
+
apiPath: string;
|
|
109
|
+
commands: CommandDefinition_2[];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export declare type CommandDefinition = CommandDefinition_2;
|
|
113
|
+
|
|
114
|
+
declare interface CommandDefinition_2 {
|
|
115
|
+
/** Method name in source casing — source of truth for HTTP routing. */
|
|
116
|
+
methodName: string;
|
|
117
|
+
/** Kebab-case identifier used by the CLI argv parser. */
|
|
118
|
+
commandName: string;
|
|
119
|
+
kind: CommandKind_2;
|
|
120
|
+
scope: CommandScope_2;
|
|
121
|
+
httpMethod: ApiHttpMethod;
|
|
122
|
+
/** URL path segments after `/<apiPath>[/<id>]/`. May be empty. */
|
|
123
|
+
pathSegments: string[];
|
|
124
|
+
description?: string;
|
|
125
|
+
/** JSONSchema describing the command's argv-flag surface. */
|
|
126
|
+
parameters?: Record<string, unknown>;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export declare type CommandKind = CommandKind_2;
|
|
130
|
+
|
|
131
|
+
declare type CommandKind_2 = 'crud' | 'custom';
|
|
132
|
+
|
|
133
|
+
export declare type CommandScope = CommandScope_2;
|
|
134
|
+
|
|
135
|
+
declare type CommandScope_2 = 'item' | 'collection';
|
|
136
|
+
|
|
137
|
+
export declare function createAppCli(options: CreateAppCliOptions): AppCli;
|
|
138
|
+
|
|
139
|
+
export declare interface CreateAppCliOptions {
|
|
140
|
+
/** Display + branding name, e.g. `willgriffin`. Also drives `envPrefix`. */
|
|
141
|
+
name: string;
|
|
142
|
+
/** Override the env-var prefix. Default: `name.toUpperCase()`. */
|
|
143
|
+
envPrefix?: string;
|
|
144
|
+
/** Override the config dir slug. Default: `name.toLowerCase()`. */
|
|
145
|
+
configDir?: string;
|
|
146
|
+
/** Baked-in default server URL when neither env nor config sets one. */
|
|
147
|
+
defaultServerUrl?: string;
|
|
148
|
+
/**
|
|
149
|
+
* App-specific commands. Dispatched BEFORE built-ins (`auth`,
|
|
150
|
+
* `resources`, `mcp`) and before the resource-slug dispatcher, so an
|
|
151
|
+
* `extraCommands` entry takes precedence over anything with the same
|
|
152
|
+
* top-level argv.
|
|
153
|
+
*
|
|
154
|
+
* - **Built-in shadowing** (`extraCommands: [{ name: 'mcp' }]`): the CLI
|
|
155
|
+
* prints a one-line stderr warning on first invocation. The extra
|
|
156
|
+
* command still wins, but the warning makes the override visible.
|
|
157
|
+
*
|
|
158
|
+
* - **Resource-slug shadowing** (`extraCommands: [{ name: 'praecos' }]`
|
|
159
|
+
* when a server-side class also resolves to slug `praecos`): the
|
|
160
|
+
* extra command silently wins. There's no warning because checking
|
|
161
|
+
* would require a discovery round-trip on every invocation. Authors
|
|
162
|
+
* are responsible for namespacing their extra commands (e.g. prefix
|
|
163
|
+
* with the app name) to avoid accidental shadowing as the server's
|
|
164
|
+
* class registry grows. (#1311 review C-2.)
|
|
165
|
+
*/
|
|
166
|
+
extraCommands?: AppCliCommand[];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Wire up the stdio server. Use `runMcpStdioBridge` for a one-call entry
|
|
171
|
+
* point in `bin/` scripts; this lower-level form is exposed for tests.
|
|
172
|
+
*/
|
|
173
|
+
export declare function createMcpStdioBridge(options: McpStdioBridgeOptions): {
|
|
174
|
+
server: Server;
|
|
175
|
+
connect: () => Promise<void>;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Fetch the discovery payload.
|
|
180
|
+
*
|
|
181
|
+
* Translates a 401 into a friendlier error so the CLI can prompt the
|
|
182
|
+
* user to log in rather than dumping a raw HTTP error.
|
|
183
|
+
*/
|
|
184
|
+
export declare function fetchResourceList(context: CliConfigContext, options?: FetchResourceListOptions): Promise<ResourceListResponse>;
|
|
185
|
+
|
|
186
|
+
declare interface FetchResourceListOptions {
|
|
187
|
+
/** Endpoint path on the server. Default `/api/_resources`. */
|
|
188
|
+
path?: string;
|
|
189
|
+
/** Custom fetch (tests). */
|
|
190
|
+
fetch?: typeof fetch;
|
|
191
|
+
/** When true, the request fails if no token is available. */
|
|
192
|
+
requireAuth?: boolean;
|
|
193
|
+
/**
|
|
194
|
+
* Pre-loaded config to avoid re-reading from disk. Set by
|
|
195
|
+
* `buildAppContext`, which has already loaded it. (#1311 review P2.)
|
|
196
|
+
*/
|
|
197
|
+
loadedConfig?: CliConfig;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Find a command on a resource by its CLI-facing name. Returns
|
|
202
|
+
* `undefined` if not found.
|
|
203
|
+
*/
|
|
204
|
+
export declare function findCommand(resource: CliResource, commandName: string): CommandDefinition | undefined;
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Find a resource by slug in the discovery payload. Returns `undefined`
|
|
208
|
+
* if the slug isn't present.
|
|
209
|
+
*/
|
|
210
|
+
export declare function findResourceBySlug(response: ResourceListResponse, slug: string): CliResource | undefined;
|
|
211
|
+
|
|
212
|
+
/** Resolve the server URL: env var → config file → `defaultServerUrl`. */
|
|
213
|
+
export declare function getServerUrl(context: CliConfigContext, config?: CliConfig): Promise<string>;
|
|
214
|
+
|
|
215
|
+
/** Resolve the stored bearer token (env var wins over config). */
|
|
216
|
+
export declare function getStoredToken(context: CliConfigContext, config?: CliConfig): Promise<string | undefined>;
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Build the URL the CLI should hit for this command, plus the fetch init
|
|
220
|
+
* (headers, body) it should pass. Returns enough so callers can stream
|
|
221
|
+
* the response — they decide how to render it (see `output.ts`).
|
|
222
|
+
*/
|
|
223
|
+
export declare function invokeCommand(options: InvokeOptions): Promise<Response>;
|
|
224
|
+
|
|
225
|
+
declare interface InvokeOptions {
|
|
226
|
+
context: CliConfigContext;
|
|
227
|
+
resource: CliResource;
|
|
228
|
+
command: CommandDefinition;
|
|
229
|
+
parsed: ParsedArgs;
|
|
230
|
+
/** Positional `<id>` argument — only meaningful for item-scope commands. */
|
|
231
|
+
id?: string;
|
|
232
|
+
/** Custom fetch (tests). */
|
|
233
|
+
fetch?: typeof fetch;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Read the CLI config file. Missing file → empty config. */
|
|
237
|
+
export declare function loadCliConfig(context: CliConfigContext): Promise<CliConfig>;
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Configuration for the bridge.
|
|
241
|
+
*
|
|
242
|
+
* Combines the {@link CliConfigContext} (env var + config file resolution)
|
|
243
|
+
* with the local MCP server identity the bridge advertises to clients.
|
|
244
|
+
*/
|
|
245
|
+
export declare interface McpStdioBridgeOptions extends CliConfigContext {
|
|
246
|
+
/** MCP server identity advertised to local clients. */
|
|
247
|
+
serverInfo: {
|
|
248
|
+
name: string;
|
|
249
|
+
version: string;
|
|
250
|
+
};
|
|
251
|
+
/** Override the tools endpoint path. Defaults to `/api/mcp/tools`. */
|
|
252
|
+
toolsPath?: string;
|
|
253
|
+
/** Override the call endpoint path. Defaults to `/api/mcp/call`. */
|
|
254
|
+
callPath?: string;
|
|
255
|
+
/**
|
|
256
|
+
* Override fetch implementation (used by tests so we never hit the network).
|
|
257
|
+
*/
|
|
258
|
+
fetch?: typeof fetch;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
declare interface OutputOptions {
|
|
262
|
+
stdout?: NodeJS.WriteStream | Writable;
|
|
263
|
+
stderr?: NodeJS.WriteStream | Writable;
|
|
264
|
+
/**
|
|
265
|
+
* Override the TTY detection (tests). Defaults to `stdout.isTTY`.
|
|
266
|
+
*/
|
|
267
|
+
stdoutIsTty?: boolean;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export declare interface ParsedArgs {
|
|
271
|
+
/** Body to send (JSON serialised, for POST/PUT/PATCH). May be empty. */
|
|
272
|
+
body: Record<string, unknown>;
|
|
273
|
+
/** Query string parameters (for GET). May be empty. */
|
|
274
|
+
query: Record<string, string | string[]>;
|
|
275
|
+
/** True when the user passed a positional `'<json>'` payload — body is that. */
|
|
276
|
+
fromPositional: boolean;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
declare interface ParserOptions {
|
|
280
|
+
/** Force-treat schema as unsupported (caller already detected unsupportedness). */
|
|
281
|
+
positionalOnly?: boolean;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Render the response. Returns the desired exit code.
|
|
286
|
+
*/
|
|
287
|
+
export declare function renderResponse(response: Response, options?: OutputOptions): Promise<RenderResult>;
|
|
288
|
+
|
|
289
|
+
export declare interface RenderResult {
|
|
290
|
+
exitCode: number;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* JSON request helper that injects the stored bearer token. Returns the
|
|
295
|
+
* parsed JSON body on success; throws an `Error` with the server's `error`
|
|
296
|
+
* field (or `HTTP <status>`) on failure.
|
|
297
|
+
*/
|
|
298
|
+
export declare function requestJson<T = unknown>(context: CliConfigContext, path: string, init?: RequestInit, options?: RequestJsonOptions): Promise<T>;
|
|
299
|
+
|
|
300
|
+
/** Options for `requestJson`. */
|
|
301
|
+
export declare interface RequestJsonOptions {
|
|
302
|
+
/**
|
|
303
|
+
* When false, skip sending the Authorization header even if a token is
|
|
304
|
+
* present. Defaults to true.
|
|
305
|
+
*/
|
|
306
|
+
auth?: boolean;
|
|
307
|
+
/**
|
|
308
|
+
* When true, throw if no token is available and `auth` is not false.
|
|
309
|
+
* Defaults to false.
|
|
310
|
+
*/
|
|
311
|
+
requireAuth?: boolean;
|
|
312
|
+
/** Override the server URL for this request. */
|
|
313
|
+
serverUrl?: string;
|
|
314
|
+
/** Override the fetch implementation (tests). */
|
|
315
|
+
fetch?: typeof fetch;
|
|
316
|
+
/**
|
|
317
|
+
* Cap on response body size in bytes. The CLI buffers the full body
|
|
318
|
+
* before JSON-parsing, so without this cap a misbehaving or malicious
|
|
319
|
+
* server could OOM the CLI process with a multi-GB payload (auth and
|
|
320
|
+
* discovery both use this code path). Defaults to 10MB — large enough
|
|
321
|
+
* for any sensible discovery response, small enough that overflow is
|
|
322
|
+
* a clear signal something's wrong. Pass `Infinity` to disable.
|
|
323
|
+
* (#1311 review A4.)
|
|
324
|
+
*/
|
|
325
|
+
maxResponseBytes?: number;
|
|
326
|
+
/**
|
|
327
|
+
* Pre-loaded config to avoid re-reading from disk. Set by callers that
|
|
328
|
+
* already have it (e.g. `buildAppContext`); leave undefined for the
|
|
329
|
+
* default behavior of loading per-call. (#1311 review P2.)
|
|
330
|
+
*/
|
|
331
|
+
loadedConfig?: CliConfig;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export declare type ResourceListResponse = ResourceListResponseBody;
|
|
335
|
+
|
|
336
|
+
declare interface ResourceListResponseBody {
|
|
337
|
+
user: {
|
|
338
|
+
authenticated: boolean;
|
|
339
|
+
id?: string;
|
|
340
|
+
};
|
|
341
|
+
warnings: string[];
|
|
342
|
+
resources: CliResource_2[];
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* One-call entry point — instantiate the bridge and connect stdio. Returns
|
|
347
|
+
* a `Promise<void>` that resolves once the transport disconnects.
|
|
348
|
+
*/
|
|
349
|
+
export declare function runMcpStdioBridge(options: McpStdioBridgeOptions): Promise<void>;
|
|
350
|
+
|
|
351
|
+
/** Persist a login: writes both serverUrl and token to the config file. */
|
|
352
|
+
export declare function saveAuth(context: CliConfigContext, serverUrl: string, token: string): Promise<void>;
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Write the CLI config to disk with 0600 permissions (the token is a bearer
|
|
356
|
+
* credential — anyone who can read the file can impersonate the user).
|
|
357
|
+
*/
|
|
358
|
+
export declare function saveCliConfig(context: CliConfigContext, config: CliConfig): Promise<void>;
|
|
359
|
+
|
|
360
|
+
export declare type SchemaSupportStatus = {
|
|
361
|
+
kind: 'ok';
|
|
362
|
+
} | {
|
|
363
|
+
kind: 'unsupported';
|
|
364
|
+
reason: string;
|
|
365
|
+
} | {
|
|
366
|
+
kind: 'missing';
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
export { }
|