@happyvertical/smrt-app-cli 0.37.2 → 0.37.3
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/dist/bin/smrt-mcp-bridge.js +41 -22
- package/dist/bridge-sTKlA6Hz.js +2 -0
- package/dist/config-DB4AMYT0.js +213 -0
- package/dist/index.js +870 -964
- package/package.json +7 -7
- package/dist/config-Bgq_EQoJ.js +0 -206
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@happyvertical/smrt-app-cli",
|
|
3
|
-
"version": "0.37.
|
|
3
|
+
"version": "0.37.3",
|
|
4
4
|
"description": "Reusable CLI factory for SMRT apps — branded `<name> <resource> <command>` CLI + stdio MCP bridge with decorator-driven resource discovery",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -21,18 +21,18 @@
|
|
|
21
21
|
],
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@modelcontextprotocol/sdk": "^1.25.2",
|
|
24
|
-
"@happyvertical/smrt-users": "0.37.
|
|
24
|
+
"@happyvertical/smrt-users": "0.37.3"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
|
-
"@types/node": "
|
|
27
|
+
"@types/node": "24.13.2",
|
|
28
28
|
"typescript": "^5.9.3",
|
|
29
|
-
"vite": "^
|
|
29
|
+
"vite": "^8.1.2",
|
|
30
30
|
"vite-plugin-dts": "4.5.4",
|
|
31
|
-
"vitest": "^4.
|
|
32
|
-
"@happyvertical/smrt-core": "0.37.
|
|
31
|
+
"vitest": "^4.1.9",
|
|
32
|
+
"@happyvertical/smrt-core": "0.37.3"
|
|
33
33
|
},
|
|
34
34
|
"engines": {
|
|
35
|
-
"node": ">=24"
|
|
35
|
+
"node": ">=24.18.0"
|
|
36
36
|
},
|
|
37
37
|
"publishConfig": {
|
|
38
38
|
"registry": "https://registry.npmjs.org",
|
package/dist/config-Bgq_EQoJ.js
DELETED
|
@@ -1,206 +0,0 @@
|
|
|
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
|
-
};
|