@husar.ai/cli 0.2.12 → 0.2.14

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/MCP_SERVER.md ADDED
@@ -0,0 +1,84 @@
1
+ # MCP Server Setup (Cursor + Codex)
2
+
3
+ This guide shows how to register the Husar MCP server so your AI tools can call the CLI commands (e.g., `husar.ai copy ...`) via the `cms_parse_file` tool.
4
+
5
+ The MCP server is built into the CLI. Run it as `husar.ai mcp`.
6
+
7
+ ## Prerequisites
8
+
9
+ - Install `@husar.ai/cli` so `husar.ai` is on your PATH.
10
+ - Ensure your workspace (project root) contains `husar.json` with:
11
+ - `host`: Husar CMS base URL
12
+ - `adminToken`: Husar admin token (required for copy/upsert)
13
+ - Use `husar.ai generate` or edit `husar.json` directly to set these.
14
+
15
+ No environment variables are required. MCP reads config like the CLI and auto-detects the workspace root.
16
+
17
+ ## Cursor: `mcp.json`
18
+
19
+ Create or edit `~/.cursor/mcp.json` and add the Husar server entry.
20
+
21
+ ```json
22
+ {
23
+ "mcpServers": {
24
+ "husar": {
25
+ "command": "husar.ai",
26
+ "args": ["mcp"]
27
+ }
28
+ }
29
+ }
30
+ ```
31
+
32
+ Notes:
33
+
34
+ - Ensure `husar.ai` is on PATH (global install) or use an absolute path to the binary.
35
+
36
+ ## Codex: `config.toml`
37
+
38
+ Create or edit `~/.config/codex/config.toml` and add a server entry.
39
+
40
+ ```toml
41
+ [mcp_servers.husar]
42
+ command = "husar.ai"
43
+ args = ["mcp"]
44
+ ```
45
+
46
+ Notes:
47
+
48
+ - No env vars are required. The server detects the workspace root and reads `husar.json` like the CLI.
49
+
50
+ ## Available Tools
51
+
52
+ - `cms_parse_file`:
53
+ - Purpose: Parse a CMS file into a shape or model and upsert via CLI.
54
+ - Inputs:
55
+ - `path`: Absolute path or path relative to the detected workspace root.
56
+ - `type`: Optional, `"model" | "shape"` (default `"shape"`).
57
+ - `name`: Optional string; if omitted, derived from filename (lowercased).
58
+ - Output: JSON stringified status or error details.
59
+ - Notes: This maps to the `husar_copy` implementation; kept for compatibility.
60
+
61
+ - `husar_copy`:
62
+ - Purpose: Same as `cms_parse_file` (alias of the CLI `copy` command).
63
+ - Inputs: Same as `cms_parse_file`.
64
+ - Auth: Reads `host` and `adminToken` from `husar.json` at the workspace root (same as CLI).
65
+ - Example arguments:
66
+ - `path`: `packages/playground/src/pages/examples/ContactForm.tsx`
67
+ - `type`: `shape`
68
+ - `name`: `examples_contactform`
69
+
70
+ - `husar_generate`:
71
+ - Purpose: Generate CMS scaffolding (Zeus client, host helpers, SSR + React helpers) into `cms/`.
72
+ - Inputs:
73
+ - `folderPath`: Optional target directory relative to the detected workspace root (defaults to `"."`). The `cms/` folder is created/rewritten inside this path.
74
+ - Host config: Uses `host` from `husar.json` at the workspace root.
75
+ - Output: JSON with `{ "status": "ok", "folder": "<ABS_PATH_TO_cms>" }` or error details.
76
+ - Files written:
77
+ - `cms/zeus/index.ts`, `cms/zeus/const.ts`
78
+ - `cms/host.ts`, `cms/ssr.ts`, `cms/react.ts`
79
+
80
+ ## Troubleshooting
81
+
82
+ - CLI not found: Ensure `@husar.ai/cli` is installed globally or that `husar.ai` is on PATH.
83
+ - Config errors: Make sure `husar.json` at your workspace root contains `host` and `adminToken`.
84
+ - Workspace detection: The server walks up from its current working directory to find a `package.json` with `workspaces`. Launch it from within your project.
package/dist/cli.js CHANGED
@@ -1,10 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander';
3
- import fs, { constants } from 'node:fs/promises';
4
- import path from 'node:path';
5
- import { generateZeus } from '@husar.ai/ssr';
6
3
  import { ConfigMaker } from 'config-maker';
7
4
  import { parser } from './functions/parser.js';
5
+ import { generateCms } from './functions/generate.js';
6
+ import { startMcpServer } from './mcp.js';
8
7
  const config = new ConfigMaker('husar', { decoders: {} });
9
8
  const program = new Command();
10
9
  program.name('husar').description('HUSAR CLI for complete generation').version('0.0.5');
@@ -16,53 +15,12 @@ program
16
15
  const conf = config.get();
17
16
  const hostEnv = conf.hostEnvironmentVariable;
18
17
  const authenticationEnv = conf.authenticationEnvironmentVariable;
19
- const cmsPath = path.join(folderPath, 'cms');
20
- try {
21
- await fs.access(cmsPath, constants.F_OK);
22
- await fs.rm(cmsPath, { recursive: true, force: true });
23
- console.log('Folder deleted.');
24
- }
25
- catch (err) {
26
- }
27
- const zeusPath = path.join(cmsPath, 'zeus');
28
- await fs.mkdir(zeusPath, { recursive: true });
29
- const zeusCode = await generateZeus(new URL('content/graphql', host).toString(), {
30
- esModule: false,
31
- env: 'browser',
18
+ await generateCms({
19
+ baseFolder: folderPath,
20
+ host,
21
+ hostEnvironmentVariable: hostEnv,
22
+ authenticationEnvironmentVariable: authenticationEnv,
32
23
  });
33
- await fs.writeFile(path.join(zeusPath, 'index.ts'), zeusCode.index);
34
- await fs.writeFile(path.join(zeusPath, 'const.ts'), zeusCode.const);
35
- let hostFile = `import * as zeus from './zeus';\n`;
36
- if (hostEnv) {
37
- hostFile += `export const HUSAR_HOST = process.env.${hostEnv};\n`;
38
- }
39
- else {
40
- hostFile += `export const HUSAR_HOST =
41
- process.env.HUSAR_HOST ||
42
- process.env.NEXT_PUBLIC_HUSAR_HOST ||
43
- process.env.VITE_HUSAR_HOST || '${host}';\n`;
44
- }
45
- hostFile += `
46
- export const getCmsHost = () => {
47
- const husar_token = process.env.${authenticationEnv || 'HUSAR_API_KEY'};
48
- const host = HUSAR_HOST ? new URL('content/graphql', HUSAR_HOST).toString() : zeus.HOST;
49
- return [host, { ...(husar_token ? { headers: { husar_token } } : {}) }] as const;
50
- };
51
- `;
52
- await fs.writeFile(path.join(cmsPath, 'host.ts'), hostFile);
53
- await fs.writeFile(path.join(cmsPath, 'ssr.ts'), `import * as zeus from './zeus';
54
- import { husarClient } from '@husar.ai/ssr';
55
- import { getCmsHost } from './host';
56
-
57
- export const husar = husarClient<typeof zeus, zeus.ModelTypes>(zeus, ...getCmsHost());
58
- `);
59
- await fs.writeFile(path.join(cmsPath, 'react.ts'), `import * as zeus from './zeus';
60
- import { HusarComponents } from '@husar.ai/render';
61
- import { getCmsHost } from './host';
62
-
63
- const { Shape, View, Model, Form } = HusarComponents<typeof zeus, zeus.ModelTypes>(getCmsHost());
64
- export { Shape, View, Model, Form };
65
- `);
66
24
  });
67
25
  program
68
26
  .command('copy <inputFile> <name>')
@@ -79,5 +37,12 @@ program
79
37
  const result = await parser(inputFile, { name, type: opts?.type }, { HUSAR_MCP_HOST: conf.host, HUSAR_MCP_ADMIN_TOKEN: conf.adminToken });
80
38
  console.log(result ? 'File parsed and upserted successfully.' : 'Failed to parse and upsert file.');
81
39
  });
40
+ program
41
+ .command('mcp')
42
+ .description('Run the Husar MCP server over stdio')
43
+ .action(async () => {
44
+ await startMcpServer();
45
+ await new Promise(() => { });
46
+ });
82
47
  program.parse(process.argv);
83
48
  //# sourceMappingURL=cli.js.map
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAGA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE1C,MAAM,MAAM,GAAG,IAAI,WAAW,CAM3B,OAAO,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,CAAC;AAE9B,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;AAE9B,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,WAAW,CAAC,mCAAmC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;AAExF,OAAO;KACJ,OAAO,CAAC,uBAAuB,CAAC;KAChC,WAAW,CAAC,0CAA0C,CAAC;KACvD,MAAM,CAAC,KAAK,EAAE,aAAqB,GAAG,EAAE,EAAE;IACzC,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,eAAe,CAAC,MAAM,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC;IACzE,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,EAAE,CAAC;IAC1B,MAAM,OAAO,GAAG,IAAI,CAAC,uBAAuB,CAAC;IAC7C,MAAM,iBAAiB,GAAG,IAAI,CAAC,iCAAiC,CAAC;IACjE,MAAM,WAAW,CAAC;QAChB,UAAU,EAAE,UAAU;QACtB,IAAI;QACJ,uBAAuB,EAAE,OAAO;QAChC,iCAAiC,EAAE,iBAAiB;KACrD,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEL,OAAO;KACJ,OAAO,CAAC,yBAAyB,CAAC;KAClC,WAAW,CAAC,iEAAiE,CAAC;KAC9E,MAAM,CAAC,mBAAmB,EAAE,0BAA0B,EAAE,OAAO,CAAC;KAChE,MAAM,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE;IACtC,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,EAAE,CAAC;IAC1B,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CAAC,4DAA4D,CAAC,CAAC;IAChF,CAAC;IACD,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC;QACrB,MAAM,IAAI,KAAK,CAAC,yFAAyF,CAAC,CAAC;IAC7G,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,MAAM,CACzB,SAAS,EACT,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,EAC1B,EAAE,cAAc,EAAE,IAAI,CAAC,IAAI,EAAE,qBAAqB,EAAE,IAAI,CAAC,UAAU,EAAE,CACtE,CAAC;IACF,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,wCAAwC,CAAC,CAAC,CAAC,kCAAkC,CAAC,CAAC;AACtG,CAAC,CAAC,CAAC;AAEL,OAAO;KACJ,OAAO,CAAC,KAAK,CAAC;KACd,WAAW,CAAC,qCAAqC,CAAC;KAClD,MAAM,CAAC,KAAK,IAAI,EAAE;IACjB,MAAM,cAAc,EAAE,CAAC;IAGvB,MAAM,IAAI,OAAO,CAAO,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;AACpC,CAAC,CAAC,CAAC;AAEL,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC"}
@@ -0,0 +1,7 @@
1
+ export type GenerateCmsOptions = {
2
+ baseFolder: string;
3
+ host: string;
4
+ hostEnvironmentVariable?: string;
5
+ authenticationEnvironmentVariable?: string;
6
+ };
7
+ export declare const generateCms: (opts: GenerateCmsOptions) => Promise<string>;
@@ -0,0 +1,46 @@
1
+ import fs, { constants } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { generateZeus } from '@husar.ai/ssr';
4
+ export const generateCms = async (opts) => {
5
+ const base = path.resolve(process.cwd(), opts.baseFolder || '.');
6
+ const cmsPath = path.join(base, 'cms');
7
+ try {
8
+ await fs.access(cmsPath, constants.F_OK);
9
+ await fs.rm(cmsPath, { recursive: true, force: true });
10
+ }
11
+ catch {
12
+ }
13
+ const zeusPath = path.join(cmsPath, 'zeus');
14
+ await fs.mkdir(zeusPath, { recursive: true });
15
+ const zeusCode = await generateZeus(new URL('content/graphql', opts.host).toString(), {
16
+ esModule: false,
17
+ env: 'browser',
18
+ });
19
+ await fs.writeFile(path.join(zeusPath, 'index.ts'), zeusCode.index);
20
+ await fs.writeFile(path.join(zeusPath, 'const.ts'), zeusCode.const);
21
+ let hostFile = `import * as zeus from './zeus';\n`;
22
+ if (opts.hostEnvironmentVariable) {
23
+ hostFile += `export const HUSAR_HOST = process.env.${opts.hostEnvironmentVariable};\n`;
24
+ }
25
+ else {
26
+ hostFile += `export const HUSAR_HOST = \n process.env.HUSAR_HOST ||\n process.env.NEXT_PUBLIC_HUSAR_HOST ||\n process.env.VITE_HUSAR_HOST || '${opts.host}';\n`;
27
+ }
28
+ const authEnv = opts.authenticationEnvironmentVariable || 'HUSAR_API_KEY';
29
+ hostFile += `\nexport const getCmsHost = () => {\n const husar_token = process.env.${authEnv};\n const host = HUSAR_HOST ? new URL('content/graphql', HUSAR_HOST).toString() : zeus.HOST;\n return [host, { ...(husar_token ? { headers: { husar_token } } : {}) }] as const;\n};\n`;
30
+ await fs.writeFile(path.join(cmsPath, 'host.ts'), hostFile);
31
+ await fs.writeFile(path.join(cmsPath, 'ssr.ts'), `import * as zeus from './zeus';
32
+ import { husarClient } from '@husar.ai/ssr';
33
+ import { getCmsHost } from './host';
34
+
35
+ export const husar = husarClient<typeof zeus, zeus.ModelTypes>(zeus, ...getCmsHost());
36
+ `);
37
+ await fs.writeFile(path.join(cmsPath, 'react.ts'), `import * as zeus from './zeus';
38
+ import { HusarComponents } from '@husar.ai/render';
39
+ import { getCmsHost } from './host';
40
+
41
+ const { Shape, View, Model, Form } = HusarComponents<typeof zeus, zeus.ModelTypes>(getCmsHost());
42
+ export { Shape, View, Model, Form };
43
+ `);
44
+ return cmsPath;
45
+ };
46
+ //# sourceMappingURL=generate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generate.js","sourceRoot":"","sources":["../../src/functions/generate.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAS7C,MAAM,CAAC,MAAM,WAAW,GAAG,KAAK,EAAE,IAAwB,EAAE,EAAE;IAC5D,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,IAAI,CAAC,UAAU,IAAI,GAAG,CAAC,CAAC;IACjE,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IACvC,IAAI,CAAC;QACH,MAAM,EAAE,CAAC,MAAM,CAAC,OAAO,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC;QACzC,MAAM,EAAE,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACzD,CAAC;IAAC,MAAM,CAAC;IAET,CAAC;IAED,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAC5C,MAAM,EAAE,CAAC,KAAK,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAE9C,MAAM,QAAQ,GAAG,MAAM,YAAY,CAAC,IAAI,GAAG,CAAC,iBAAiB,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,EAAE;QACpF,QAAQ,EAAE,KAAK;QACf,GAAG,EAAE,SAAS;KACf,CAAC,CAAC;IACH,MAAM,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,UAAU,CAAC,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC;IACpE,MAAM,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,UAAU,CAAC,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC;IAEpE,IAAI,QAAQ,GAAG,mCAAmC,CAAC;IACnD,IAAI,IAAI,CAAC,uBAAuB,EAAE,CAAC;QACjC,QAAQ,IAAI,yCAAyC,IAAI,CAAC,uBAAuB,KAAK,CAAC;IACzF,CAAC;SAAM,CAAC;QACN,QAAQ,IAAI,uIAAuI,IAAI,CAAC,IAAI,MAAM,CAAC;IACrK,CAAC;IACD,MAAM,OAAO,GAAG,IAAI,CAAC,iCAAiC,IAAI,eAAe,CAAC;IAC1E,QAAQ,IAAI,0EAA0E,OAAO,0LAA0L,CAAC;IACxR,MAAM,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,EAAE,QAAQ,CAAC,CAAC;IAE5D,MAAM,EAAE,CAAC,SAAS,CAChB,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,EAC5B;;;;;CAKH,CACE,CAAC;IAEF,MAAM,EAAE,CAAC,SAAS,CAChB,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,UAAU,CAAC,EAC9B;;;;;;CAMH,CACE,CAAC;IAEF,OAAO,OAAO,CAAC;AACjB,CAAC,CAAC"}
package/dist/mcp.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export declare const startMcpServer: () => Promise<void>;
package/dist/mcp.js ADDED
@@ -0,0 +1,228 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import * as fs from 'node:fs/promises';
5
+ import * as path from 'node:path';
6
+ import { z } from 'zod';
7
+ import { ConfigMaker } from 'config-maker';
8
+ import { spawn } from 'node:child_process';
9
+ const server = new McpServer({ name: 'mcp-husar', version: '1.0.0' });
10
+ let config;
11
+ const getWorkspaceRoot = async () => {
12
+ const envRoot = process.env.WORKSPACE_ROOT;
13
+ if (envRoot)
14
+ return envRoot;
15
+ let cur = process.cwd();
16
+ for (let i = 0; i < 10; i++) {
17
+ try {
18
+ const pkgPath = path.join(cur, 'package.json');
19
+ const st = await fs.stat(pkgPath);
20
+ if (st.isFile()) {
21
+ const txt = await fs.readFile(pkgPath, 'utf8');
22
+ const pkg = JSON.parse(txt);
23
+ if (pkg && Object.prototype.hasOwnProperty.call(pkg, 'workspaces'))
24
+ return cur;
25
+ }
26
+ }
27
+ catch {
28
+ }
29
+ const parent = path.dirname(cur);
30
+ if (parent === cur)
31
+ break;
32
+ cur = parent;
33
+ }
34
+ return process.cwd();
35
+ };
36
+ const resolveWorkspacePath = async (p) => {
37
+ if (path.isAbsolute(p))
38
+ return p;
39
+ const root = await getWorkspaceRoot();
40
+ return path.resolve(root, p);
41
+ };
42
+ const getConfig = async () => {
43
+ if (config)
44
+ return config;
45
+ const root = await getWorkspaceRoot();
46
+ config = new ConfigMaker('husar', {
47
+ decoders: {},
48
+ pathToProject: root,
49
+ });
50
+ return config;
51
+ };
52
+ const getAuth = async () => {
53
+ const envHost = process.env.HUSAR_MCP_HOST;
54
+ const envToken = process.env.HUSAR_MCP_ADMIN_TOKEN;
55
+ if (envHost && envToken)
56
+ return { host: envHost, adminToken: envToken };
57
+ try {
58
+ const cfg = await getConfig();
59
+ const { host, adminToken } = cfg.get();
60
+ if (host && adminToken)
61
+ return { host, adminToken };
62
+ }
63
+ catch {
64
+ }
65
+ throw new Error('Missing HUSAR_MCP_HOST and/or HUSAR_MCP_ADMIN_TOKEN. Provide via env or husar.json in workspace root. ' +
66
+ (await getWorkspaceRoot()));
67
+ };
68
+ const cmsParseFileInputFields = {
69
+ path: z
70
+ .string()
71
+ .min(1)
72
+ .describe('Path to the component/file. Accepts absolute paths or paths relative to the workspace root. Examples: "packages/playground/src/pages/examples/ContactForm.tsx" or "/home/user/repo/.../FeatureGrid.tsx". Supported: .tsx/.jsx/.ts/.js/.html'),
73
+ type: z
74
+ .enum(['model', 'shape'])
75
+ .optional()
76
+ .default('shape')
77
+ .describe('Target in CMS. Use "shape" for UI/component structures (default), or "model" for data models.'),
78
+ name: z
79
+ .string()
80
+ .optional()
81
+ .describe('Optional CMS name. Must not contain dashes (-). Prefer lowercase with underscores (e.g., examples_contactform). Defaults to the filename (lowercased).'),
82
+ };
83
+ const cmsParseFileInputSchema = z.object(cmsParseFileInputFields);
84
+ const withTimeout = async (promise, ms) => {
85
+ let timeoutId;
86
+ try {
87
+ return await Promise.race([
88
+ promise,
89
+ new Promise((_, reject) => {
90
+ timeoutId = setTimeout(() => reject(new Error(`Operation timed out after ${ms} ms`)), ms);
91
+ }),
92
+ ]);
93
+ }
94
+ finally {
95
+ if (timeoutId)
96
+ clearTimeout(timeoutId);
97
+ }
98
+ };
99
+ const redirectConsoleToStderr = () => {
100
+ const original = {
101
+ log: console.log,
102
+ info: console.info,
103
+ debug: console.debug,
104
+ dir: console.dir,
105
+ };
106
+ console.log = (...args) => process.stderr.write(args.map(String).join(' ') + '\n');
107
+ console.info = (...args) => process.stderr.write(args.map(String).join(' ') + '\n');
108
+ console.debug = (...args) => process.stderr.write(args.map(String).join(' ') + '\n');
109
+ console.dir = (obj) => {
110
+ try {
111
+ const text = typeof obj === 'string' ? obj : JSON.stringify(obj, null, 2);
112
+ process.stderr.write(text + '\n');
113
+ }
114
+ catch {
115
+ process.stderr.write(String(obj) + '\n');
116
+ }
117
+ return obj;
118
+ };
119
+ return () => {
120
+ console.log = original.log;
121
+ console.info = original.info;
122
+ console.debug = original.debug;
123
+ console.dir = original.dir;
124
+ };
125
+ };
126
+ const runCli = async (args, workspaceCwd) => {
127
+ const npxBin = process.platform === 'win32' ? 'npx.cmd' : 'npx';
128
+ return await new Promise((resolve) => {
129
+ const child = spawn(npxBin, ['-y', '@husar.ai/cli', ...args], {
130
+ cwd: workspaceCwd,
131
+ stdio: ['ignore', 'pipe', 'pipe'],
132
+ env: { ...process.env },
133
+ shell: false,
134
+ });
135
+ let stdout = '';
136
+ let stderr = '';
137
+ child.stdout.on('data', (d) => (stdout += String(d)));
138
+ child.stderr.on('data', (d) => (stderr += String(d)));
139
+ child.on('close', (code) => resolve({ code: code ?? 1, stdout, stderr }));
140
+ });
141
+ };
142
+ const createCmsParseFileHandler = ({ timeoutMs = 30_000 }) => {
143
+ return async ({ path: file, type, name: passed }) => {
144
+ const restoreConsole = redirectConsoleToStderr();
145
+ try {
146
+ await getAuth();
147
+ const absPath = await resolveWorkspacePath(file);
148
+ const root = await getWorkspaceRoot();
149
+ const name = (passed ?? (absPath.replace(/^.*\/(.*?)(\.[^.]+)?$/, '$1') || 'untitled')).toLowerCase();
150
+ const res = await withTimeout(runCli(['copy', absPath, name, '-t', type ?? 'shape'], root), timeoutMs);
151
+ if (res.code !== 0) {
152
+ throw new Error(`CLI exited with code ${res.code}.\nSTDOUT:\n${res.stdout}\nSTDERR:\n${res.stderr}`);
153
+ }
154
+ return { content: [{ type: 'text', text: res.stdout || 'ok' }] };
155
+ }
156
+ catch (err) {
157
+ const message = err instanceof Error ? err.message : String(err);
158
+ const stack = err instanceof Error && err.stack ? `\nStack: ${err.stack}` : '';
159
+ return { isError: true, content: [{ type: 'text', text: `Error: ${message}${stack}` }] };
160
+ }
161
+ finally {
162
+ try {
163
+ restoreConsole();
164
+ }
165
+ catch {
166
+ }
167
+ }
168
+ };
169
+ };
170
+ export const startMcpServer = async () => {
171
+ server.tool('cms_parse_file', 'Parse a CMS file into a shape or model', cmsParseFileInputFields, createCmsParseFileHandler({ timeoutMs: Number(process.env.MCP_PARSER_TIMEOUT_MS || 30000) }));
172
+ server.tool('husar_copy', 'Copy: parse HTML/JSX and upsert into CMS (model/shape)', cmsParseFileInputFields, createCmsParseFileHandler({ timeoutMs: Number(process.env.MCP_PARSER_TIMEOUT_MS || 30000) }));
173
+ const husarGenerateInput = {
174
+ folderPath: z
175
+ .string()
176
+ .optional()
177
+ .describe('Target folder for cms scaffolding. Relative to workspace root; defaults to ".".'),
178
+ };
179
+ const husarGenerateSchema = z.object(husarGenerateInput);
180
+ server.tool('husar_generate', 'Generate cms structure in the given path', husarGenerateInput, (async (args) => {
181
+ try {
182
+ const { folderPath } = husarGenerateSchema.parse(args ?? {});
183
+ const root = await getWorkspaceRoot();
184
+ await getAuth();
185
+ const argsList = ['generate'];
186
+ if (folderPath)
187
+ argsList.push(folderPath);
188
+ const res = await runCli(argsList, root);
189
+ if (res.code !== 0) {
190
+ throw new Error(`CLI exited with code ${res.code}.\nSTDOUT:\n${res.stdout}\nSTDERR:\n${res.stderr}`);
191
+ }
192
+ return { content: [{ type: 'text', text: res.stdout || 'ok' }] };
193
+ }
194
+ catch (err) {
195
+ const message = err instanceof Error ? err.message : String(err);
196
+ const stack = err instanceof Error && err.stack ? `\nStack: ${err.stack}` : '';
197
+ return { isError: true, content: [{ type: 'text', text: `Error: ${message}${stack}` }] };
198
+ }
199
+ }));
200
+ const transport = new StdioServerTransport();
201
+ server.connect(transport);
202
+ try {
203
+ if (typeof process.stdin.resume === 'function')
204
+ process.stdin.resume();
205
+ }
206
+ catch {
207
+ }
208
+ process.on('uncaughtException', (err) => {
209
+ const message = err instanceof Error ? `${err.message}\n${err.stack ?? ''}` : String(err);
210
+ try {
211
+ process.stderr.write(`[uncaughtException] ${message}\n`);
212
+ }
213
+ catch {
214
+ }
215
+ });
216
+ process.on('unhandledRejection', (reason) => {
217
+ const message = reason instanceof Error ? `${reason.message}\n${reason.stack ?? ''}` : String(reason);
218
+ try {
219
+ process.stderr.write(`[unhandledRejection] ${message}\n`);
220
+ }
221
+ catch {
222
+ }
223
+ });
224
+ };
225
+ if (import.meta.url === `file://${process.argv[1]}`) {
226
+ void startMcpServer();
227
+ }
228
+ //# sourceMappingURL=mcp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp.js","sourceRoot":"","sources":["../src/mcp.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,27 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { spawn } from 'node:child_process';
6
+ const here = path.dirname(fileURLToPath(import.meta.url));
7
+ const repoRoot = path.resolve(here, '../../..');
8
+ test.skip('mcp server starts and stays alive briefly', async () => {
9
+ const child = spawn('node', [path.resolve(path.dirname(here), '../dist/mcp.js')], {
10
+ cwd: repoRoot,
11
+ env: {
12
+ ...process.env,
13
+ WORKSPACE_ROOT: repoRoot,
14
+ HUSAR_MCP_HOST: '',
15
+ HUSAR_MCP_ADMIN_TOKEN: '',
16
+ },
17
+ stdio: ['pipe', 'pipe', 'pipe'],
18
+ });
19
+ let exited = false;
20
+ child.on('exit', () => {
21
+ exited = true;
22
+ });
23
+ await new Promise((r) => setTimeout(r, 300));
24
+ assert.equal(exited, false);
25
+ child.kill('SIGKILL');
26
+ });
27
+ //# sourceMappingURL=mcp.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp.test.js","sourceRoot":"","sources":["../src/mcp.test.ts"],"names":[],"mappings":"AACA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAE3C,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAC1D,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;AAEhD,IAAI,CAAC,IAAI,CAAC,2CAA2C,EAAE,KAAK,IAAI,EAAE;IAChE,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,gBAAgB,CAAC,CAAC,EAAE;QAChF,GAAG,EAAE,QAAQ;QACb,GAAG,EAAE;YACH,GAAG,OAAO,CAAC,GAAG;YACd,cAAc,EAAE,QAAQ;YACxB,cAAc,EAAE,EAAE;YAClB,qBAAqB,EAAE,EAAE;SAC1B;QACD,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;KAChC,CAAC,CAAC;IACH,IAAI,MAAM,GAAG,KAAK,CAAC;IACnB,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE;QACpB,MAAM,GAAG,IAAI,CAAC;IAChB,CAAC,CAAC,CAAC;IAEH,MAAM,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;IAC7C,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IAE5B,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;AACxB,CAAC,CAAC,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,28 @@
1
+ import path from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
4
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
5
+ const here = path.dirname(fileURLToPath(import.meta.url));
6
+ const repoRoot = path.resolve(here, '../../../..');
7
+ const cliDist = path.resolve(repoRoot, 'packages/cli/dist/mcp.js');
8
+ const landing = path.resolve(repoRoot, 'packages/landing');
9
+ async function main() {
10
+ const transport = new StdioClientTransport({
11
+ command: 'node',
12
+ args: [cliDist],
13
+ cwd: landing,
14
+ env: {
15
+ ...process.env,
16
+ WORKSPACE_ROOT: landing,
17
+ },
18
+ stderr: 'pipe',
19
+ });
20
+ const client = new Client({ name: 'husar-mcp-smoke', version: '0.0.0' });
21
+ await client.connect(transport);
22
+ const res = await client.callTool({ name: 'husar_generate', arguments: {} });
23
+ const text = res?.content?.[0]?.text || '';
24
+ console.log('husar_generate result:', text);
25
+ await client.close();
26
+ }
27
+ void main();
28
+ //# sourceMappingURL=mcp-generate-test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp-generate-test.js","sourceRoot":"","sources":["../../src/scripts/mcp-generate-test.ts"],"names":[],"mappings":"AACA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACnE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAEjF,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAC1D,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,aAAa,CAAC,CAAC;AACnD,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,0BAA0B,CAAC,CAAC;AACnE,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,kBAAkB,CAAC,CAAC;AAE3D,KAAK,UAAU,IAAI;IACjB,MAAM,SAAS,GAAG,IAAI,oBAAoB,CAAC;QACzC,OAAO,EAAE,MAAM;QACf,IAAI,EAAE,CAAC,OAAO,CAAC;QACf,GAAG,EAAE,OAAO;QACZ,GAAG,EAAE;YACH,GAAG,OAAO,CAAC,GAAG;YACd,cAAc,EAAE,OAAO;SACxB;QACD,MAAM,EAAE,MAAM;KACf,CAAC,CAAC;IACH,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC,EAAE,IAAI,EAAE,iBAAiB,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;IACzE,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAEhC,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,gBAAgB,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC,CAAC;IAC7E,MAAM,IAAI,GAAI,GAAW,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,IAAI,EAAE,CAAC;IACpD,OAAO,CAAC,GAAG,CAAC,wBAAwB,EAAE,IAAI,CAAC,CAAC;IAC5C,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;AACvB,CAAC;AAGD,KAAK,IAAI,EAAE,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@husar.ai/cli",
3
- "version": "0.2.12",
3
+ "version": "0.2.14",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",
@@ -12,12 +12,15 @@
12
12
  },
13
13
  "scripts": {
14
14
  "build": "tspc",
15
+ "test": "node --test --import tsx ./src/**/*.test.ts",
15
16
  "watch": "tspc --watch"
16
17
  },
17
18
  "dependencies": {
18
- "@husar.ai/ssr": "^0.2.12",
19
+ "@husar.ai/ssr": "^0.2.14",
20
+ "@modelcontextprotocol/sdk": "^1.17.4",
19
21
  "commander": "^11.0.0",
20
- "config-maker": "^0.0.6"
22
+ "config-maker": "^0.0.6",
23
+ "zod": "^3.23.8"
21
24
  },
22
25
  "devDependencies": {}
23
26
  }
package/src/cli.ts CHANGED
@@ -2,11 +2,11 @@
2
2
  /* eslint-disable no-useless-escape */
3
3
  /* eslint-disable @typescript-eslint/no-explicit-any */
4
4
  import { Command } from 'commander';
5
- import fs, { constants } from 'node:fs/promises';
6
- import path from 'node:path';
7
- import { generateZeus } from '@husar.ai/ssr';
5
+ // nofs
8
6
  import { ConfigMaker } from 'config-maker';
9
7
  import { parser } from './functions/parser.js';
8
+ import { generateCms } from './functions/generate.js';
9
+ import { startMcpServer } from './mcp.js';
10
10
 
11
11
  const config = new ConfigMaker<{
12
12
  host: string;
@@ -28,58 +28,12 @@ program
28
28
  const conf = config.get();
29
29
  const hostEnv = conf.hostEnvironmentVariable;
30
30
  const authenticationEnv = conf.authenticationEnvironmentVariable;
31
- const cmsPath = path.join(folderPath, 'cms');
32
- try {
33
- await fs.access(cmsPath, constants.F_OK); // Check if folder exists
34
- await fs.rm(cmsPath, { recursive: true, force: true });
35
- console.log('Folder deleted.');
36
- } catch (err: any) {
37
- // noop
38
- }
39
- const zeusPath = path.join(cmsPath, 'zeus');
40
- await fs.mkdir(zeusPath, { recursive: true });
41
- const zeusCode = await generateZeus(new URL('content/graphql', host).toString(), {
42
- esModule: false,
43
- env: 'browser',
31
+ await generateCms({
32
+ baseFolder: folderPath,
33
+ host,
34
+ hostEnvironmentVariable: hostEnv,
35
+ authenticationEnvironmentVariable: authenticationEnv,
44
36
  });
45
- await fs.writeFile(path.join(zeusPath, 'index.ts'), zeusCode.index);
46
- await fs.writeFile(path.join(zeusPath, 'const.ts'), zeusCode.const);
47
- let hostFile = `import * as zeus from './zeus';\n`;
48
- if (hostEnv) {
49
- hostFile += `export const HUSAR_HOST = process.env.${hostEnv};\n`;
50
- } else {
51
- hostFile += `export const HUSAR_HOST =
52
- process.env.HUSAR_HOST ||
53
- process.env.NEXT_PUBLIC_HUSAR_HOST ||
54
- process.env.VITE_HUSAR_HOST || '${host}';\n`;
55
- }
56
- hostFile += `
57
- export const getCmsHost = () => {
58
- const husar_token = process.env.${authenticationEnv || 'HUSAR_API_KEY'};
59
- const host = HUSAR_HOST ? new URL('content/graphql', HUSAR_HOST).toString() : zeus.HOST;
60
- return [host, { ...(husar_token ? { headers: { husar_token } } : {}) }] as const;
61
- };
62
- `;
63
- await fs.writeFile(path.join(cmsPath, 'host.ts'), hostFile);
64
- await fs.writeFile(
65
- path.join(cmsPath, 'ssr.ts'),
66
- `import * as zeus from './zeus';
67
- import { husarClient } from '@husar.ai/ssr';
68
- import { getCmsHost } from './host';
69
-
70
- export const husar = husarClient<typeof zeus, zeus.ModelTypes>(zeus, ...getCmsHost());
71
- `,
72
- );
73
- await fs.writeFile(
74
- path.join(cmsPath, 'react.ts'),
75
- `import * as zeus from './zeus';
76
- import { HusarComponents } from '@husar.ai/render';
77
- import { getCmsHost } from './host';
78
-
79
- const { Shape, View, Model, Form } = HusarComponents<typeof zeus, zeus.ModelTypes>(getCmsHost());
80
- export { Shape, View, Model, Form };
81
- `,
82
- );
83
37
  });
84
38
 
85
39
  program
@@ -102,4 +56,14 @@ program
102
56
  console.log(result ? 'File parsed and upserted successfully.' : 'Failed to parse and upsert file.');
103
57
  });
104
58
 
59
+ program
60
+ .command('mcp')
61
+ .description('Run the Husar MCP server over stdio')
62
+ .action(async () => {
63
+ await startMcpServer();
64
+ // keep process alive
65
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
66
+ await new Promise<void>(() => {});
67
+ });
68
+
105
69
  program.parse(process.argv);
@@ -0,0 +1,66 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import fs, { constants } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { generateZeus } from '@husar.ai/ssr';
5
+
6
+ export type GenerateCmsOptions = {
7
+ baseFolder: string; // absolute or relative to process.cwd()
8
+ host: string; // base URL, e.g., https://example.com
9
+ hostEnvironmentVariable?: string;
10
+ authenticationEnvironmentVariable?: string; // env var name for token
11
+ };
12
+
13
+ export const generateCms = async (opts: GenerateCmsOptions) => {
14
+ const base = path.resolve(process.cwd(), opts.baseFolder || '.');
15
+ const cmsPath = path.join(base, 'cms');
16
+ try {
17
+ await fs.access(cmsPath, constants.F_OK);
18
+ await fs.rm(cmsPath, { recursive: true, force: true });
19
+ } catch {
20
+ // ignore if not exists
21
+ }
22
+
23
+ const zeusPath = path.join(cmsPath, 'zeus');
24
+ await fs.mkdir(zeusPath, { recursive: true });
25
+
26
+ const zeusCode = await generateZeus(new URL('content/graphql', opts.host).toString(), {
27
+ esModule: false,
28
+ env: 'browser',
29
+ });
30
+ await fs.writeFile(path.join(zeusPath, 'index.ts'), zeusCode.index);
31
+ await fs.writeFile(path.join(zeusPath, 'const.ts'), zeusCode.const);
32
+
33
+ let hostFile = `import * as zeus from './zeus';\n`;
34
+ if (opts.hostEnvironmentVariable) {
35
+ hostFile += `export const HUSAR_HOST = process.env.${opts.hostEnvironmentVariable};\n`;
36
+ } else {
37
+ hostFile += `export const HUSAR_HOST = \n process.env.HUSAR_HOST ||\n process.env.NEXT_PUBLIC_HUSAR_HOST ||\n process.env.VITE_HUSAR_HOST || '${opts.host}';\n`;
38
+ }
39
+ const authEnv = opts.authenticationEnvironmentVariable || 'HUSAR_API_KEY';
40
+ hostFile += `\nexport const getCmsHost = () => {\n const husar_token = process.env.${authEnv};\n const host = HUSAR_HOST ? new URL('content/graphql', HUSAR_HOST).toString() : zeus.HOST;\n return [host, { ...(husar_token ? { headers: { husar_token } } : {}) }] as const;\n};\n`;
41
+ await fs.writeFile(path.join(cmsPath, 'host.ts'), hostFile);
42
+
43
+ await fs.writeFile(
44
+ path.join(cmsPath, 'ssr.ts'),
45
+ `import * as zeus from './zeus';
46
+ import { husarClient } from '@husar.ai/ssr';
47
+ import { getCmsHost } from './host';
48
+
49
+ export const husar = husarClient<typeof zeus, zeus.ModelTypes>(zeus, ...getCmsHost());
50
+ `,
51
+ );
52
+
53
+ await fs.writeFile(
54
+ path.join(cmsPath, 'react.ts'),
55
+ `import * as zeus from './zeus';
56
+ import { HusarComponents } from '@husar.ai/render';
57
+ import { getCmsHost } from './host';
58
+
59
+ const { Shape, View, Model, Form } = HusarComponents<typeof zeus, zeus.ModelTypes>(getCmsHost());
60
+ export { Shape, View, Model, Form };
61
+ `,
62
+ );
63
+
64
+ return cmsPath;
65
+ };
66
+
@@ -0,0 +1,31 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import test from 'node:test';
3
+ import assert from 'node:assert/strict';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { spawn } from 'node:child_process';
7
+
8
+ const here = path.dirname(fileURLToPath(import.meta.url));
9
+ const repoRoot = path.resolve(here, '../../..');
10
+
11
+ test.skip('mcp server starts and stays alive briefly', async () => {
12
+ const child = spawn('node', [path.resolve(path.dirname(here), '../dist/mcp.js')], {
13
+ cwd: repoRoot,
14
+ env: {
15
+ ...process.env,
16
+ WORKSPACE_ROOT: repoRoot,
17
+ HUSAR_MCP_HOST: '',
18
+ HUSAR_MCP_ADMIN_TOKEN: '',
19
+ },
20
+ stdio: ['pipe', 'pipe', 'pipe'],
21
+ });
22
+ let exited = false;
23
+ child.on('exit', () => {
24
+ exited = true;
25
+ });
26
+ // wait a short period to ensure the process stays running
27
+ await new Promise((r) => setTimeout(r, 300));
28
+ assert.equal(exited, false);
29
+ // cleanup
30
+ child.kill('SIGKILL');
31
+ });
package/src/mcp.ts ADDED
@@ -0,0 +1,263 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-disable @typescript-eslint/no-explicit-any */
3
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
+ import * as fs from 'node:fs/promises';
6
+ import * as path from 'node:path';
7
+ import { z } from 'zod';
8
+ import { ConfigMaker } from 'config-maker';
9
+ import { spawn } from 'node:child_process';
10
+
11
+ const server = new McpServer({ name: 'mcp-husar', version: '1.0.0' });
12
+
13
+ let config: ConfigMaker<{ host: string; adminToken: string }> | undefined;
14
+
15
+ const getWorkspaceRoot = async (): Promise<string> => {
16
+ const envRoot = process.env.WORKSPACE_ROOT;
17
+ if (envRoot) return envRoot;
18
+ let cur = process.cwd();
19
+ for (let i = 0; i < 10; i++) {
20
+ try {
21
+ const pkgPath = path.join(cur, 'package.json');
22
+ const st = await fs.stat(pkgPath);
23
+ if (st.isFile()) {
24
+ const txt = await fs.readFile(pkgPath, 'utf8');
25
+ const pkg = JSON.parse(txt) as { workspaces?: unknown };
26
+ if (pkg && Object.prototype.hasOwnProperty.call(pkg, 'workspaces')) return cur;
27
+ }
28
+ } catch {
29
+ /* empty */
30
+ }
31
+ const parent = path.dirname(cur);
32
+ if (parent === cur) break;
33
+ cur = parent;
34
+ }
35
+ return process.cwd();
36
+ };
37
+
38
+ const resolveWorkspacePath = async (p: string): Promise<string> => {
39
+ if (path.isAbsolute(p)) return p;
40
+ const root = await getWorkspaceRoot();
41
+ return path.resolve(root, p);
42
+ };
43
+
44
+ const getConfig = async () => {
45
+ if (config) return config;
46
+ const root = await getWorkspaceRoot();
47
+ config = new ConfigMaker<{ host: string; adminToken: string }>('husar', {
48
+ decoders: {},
49
+ pathToProject: root,
50
+ });
51
+ return config;
52
+ };
53
+
54
+ const getAuth = async () => {
55
+ const envHost = process.env.HUSAR_MCP_HOST;
56
+ const envToken = process.env.HUSAR_MCP_ADMIN_TOKEN;
57
+ if (envHost && envToken) return { host: envHost, adminToken: envToken } as const;
58
+
59
+ // Non-interactive: read current config values only
60
+ try {
61
+ const cfg = await getConfig();
62
+ const { host, adminToken } = cfg.get();
63
+ if (host && adminToken) return { host, adminToken } as const;
64
+ } catch {
65
+ // ignore; we'll throw below
66
+ }
67
+
68
+ throw new Error(
69
+ 'Missing HUSAR_MCP_HOST and/or HUSAR_MCP_ADMIN_TOKEN. Provide via env or husar.json in workspace root. ' +
70
+ (await getWorkspaceRoot()),
71
+ );
72
+ };
73
+
74
+ const cmsParseFileInputFields = {
75
+ path: z
76
+ .string()
77
+ .min(1)
78
+ .describe(
79
+ 'Path to the component/file. Accepts absolute paths or paths relative to the workspace root. Examples: "packages/playground/src/pages/examples/ContactForm.tsx" or "/home/user/repo/.../FeatureGrid.tsx". Supported: .tsx/.jsx/.ts/.js/.html',
80
+ ),
81
+ type: z
82
+ .enum(['model', 'shape'])
83
+ .optional()
84
+ .default('shape')
85
+ .describe('Target in CMS. Use "shape" for UI/component structures (default), or "model" for data models.'),
86
+ name: z
87
+ .string()
88
+ .optional()
89
+ .describe(
90
+ 'Optional CMS name. Must not contain dashes (-). Prefer lowercase with underscores (e.g., examples_contactform). Defaults to the filename (lowercased).',
91
+ ),
92
+ };
93
+
94
+ const cmsParseFileInputSchema = z.object(cmsParseFileInputFields);
95
+ type CmsParseFileInput = z.infer<typeof cmsParseFileInputSchema>;
96
+
97
+ const withTimeout = async <T>(promise: Promise<T>, ms: number): Promise<T> => {
98
+ let timeoutId: NodeJS.Timeout | undefined;
99
+ try {
100
+ return await Promise.race([
101
+ promise,
102
+ new Promise<T>((_, reject) => {
103
+ timeoutId = setTimeout(() => reject(new Error(`Operation timed out after ${ms} ms`)), ms);
104
+ }),
105
+ ]);
106
+ } finally {
107
+ if (timeoutId) clearTimeout(timeoutId);
108
+ }
109
+ };
110
+
111
+ const redirectConsoleToStderr = () => {
112
+ const original = {
113
+ log: console.log,
114
+ info: console.info,
115
+ debug: console.debug,
116
+ dir: console.dir,
117
+ } as const;
118
+ console.log = (...args: unknown[]) => process.stderr.write(args.map(String).join(' ') + '\n');
119
+ console.info = (...args: unknown[]) => process.stderr.write(args.map(String).join(' ') + '\n');
120
+ console.debug = (...args: unknown[]) => process.stderr.write(args.map(String).join(' ') + '\n');
121
+ console.dir = (obj: unknown) => {
122
+ try {
123
+ const text = typeof obj === 'string' ? obj : JSON.stringify(obj, null, 2);
124
+ process.stderr.write(text + '\n');
125
+ } catch {
126
+ process.stderr.write(String(obj) + '\n');
127
+ }
128
+ return obj as never;
129
+ };
130
+ return () => {
131
+ console.log = original.log;
132
+ console.info = original.info;
133
+ console.debug = original.debug;
134
+ console.dir = original.dir;
135
+ };
136
+ };
137
+
138
+ const runCli = async (args: string[], workspaceCwd: string) => {
139
+ const npxBin = process.platform === 'win32' ? 'npx.cmd' : 'npx';
140
+ return await new Promise<{ code: number; stdout: string; stderr: string }>((resolve) => {
141
+ const child = spawn(npxBin, ['-y', '@husar.ai/cli', ...args], {
142
+ cwd: workspaceCwd,
143
+ stdio: ['ignore', 'pipe', 'pipe'],
144
+ env: { ...process.env },
145
+ shell: false,
146
+ });
147
+ let stdout = '';
148
+ let stderr = '';
149
+ child.stdout.on('data', (d) => (stdout += String(d)));
150
+ child.stderr.on('data', (d) => (stderr += String(d)));
151
+ child.on('close', (code) => resolve({ code: code ?? 1, stdout, stderr }));
152
+ });
153
+ };
154
+
155
+ const createCmsParseFileHandler = ({ timeoutMs = 30_000 }: { timeoutMs?: number }) => {
156
+ return async ({ path: file, type, name: passed }: CmsParseFileInput) => {
157
+ const restoreConsole = redirectConsoleToStderr();
158
+ try {
159
+ // Ensure config exists or env is set; if missing, CLI will error
160
+ await getAuth();
161
+ const absPath = await resolveWorkspacePath(file);
162
+ const root = await getWorkspaceRoot();
163
+ const name = (passed ?? (absPath.replace(/^.*\/(.*?)(\.[^.]+)?$/, '$1') || 'untitled')).toLowerCase();
164
+ const res = await withTimeout(runCli(['copy', absPath, name, '-t', type ?? 'shape'], root), timeoutMs);
165
+ if (res.code !== 0) {
166
+ throw new Error(`CLI exited with code ${res.code}.\nSTDOUT:\n${res.stdout}\nSTDERR:\n${res.stderr}`);
167
+ }
168
+ return { content: [{ type: 'text', text: res.stdout || 'ok' }] };
169
+ } catch (err) {
170
+ const message = err instanceof Error ? err.message : String(err);
171
+ const stack = err instanceof Error && err.stack ? `\nStack: ${err.stack}` : '';
172
+ return { isError: true, content: [{ type: 'text', text: `Error: ${message}${stack}` }] };
173
+ } finally {
174
+ try {
175
+ restoreConsole();
176
+ } catch {
177
+ /* no-op */
178
+ }
179
+ }
180
+ };
181
+ };
182
+
183
+ export const startMcpServer = async () => {
184
+ // Tool: cms_parse_file (legacy)
185
+ server.tool(
186
+ 'cms_parse_file',
187
+ 'Parse a CMS file into a shape or model',
188
+ cmsParseFileInputFields,
189
+ createCmsParseFileHandler({ timeoutMs: Number(process.env.MCP_PARSER_TIMEOUT_MS || 30000) }) as any,
190
+ );
191
+
192
+ // Tool: husar_copy (alias)
193
+ server.tool(
194
+ 'husar_copy',
195
+ 'Copy: parse HTML/JSX and upsert into CMS (model/shape)',
196
+ cmsParseFileInputFields,
197
+ createCmsParseFileHandler({ timeoutMs: Number(process.env.MCP_PARSER_TIMEOUT_MS || 30000) }) as any,
198
+ );
199
+
200
+ // Tool: husar_generate
201
+ const husarGenerateInput = {
202
+ folderPath: z
203
+ .string()
204
+ .optional()
205
+ .describe('Target folder for cms scaffolding. Relative to workspace root; defaults to ".".'),
206
+ } as const;
207
+ const husarGenerateSchema = z.object(husarGenerateInput);
208
+ server.tool('husar_generate', 'Generate cms structure in the given path', husarGenerateInput, (async (
209
+ args: unknown,
210
+ ) => {
211
+ try {
212
+ const { folderPath } = husarGenerateSchema.parse(args ?? {});
213
+ const root = await getWorkspaceRoot();
214
+ // Ensure config exists; if missing, CLI will error
215
+ await getAuth();
216
+ const argsList = ['generate'];
217
+ if (folderPath) argsList.push(folderPath);
218
+ const res = await runCli(argsList, root);
219
+ if (res.code !== 0) {
220
+ throw new Error(`CLI exited with code ${res.code}.\nSTDOUT:\n${res.stdout}\nSTDERR:\n${res.stderr}`);
221
+ }
222
+ // The CLI doesn't print a JSON payload; return a simple ok message
223
+ return { content: [{ type: 'text', text: res.stdout || 'ok' }] };
224
+ } catch (err) {
225
+ const message = err instanceof Error ? err.message : String(err);
226
+ const stack = err instanceof Error && err.stack ? `\nStack: ${err.stack}` : '';
227
+ return { isError: true, content: [{ type: 'text', text: `Error: ${message}${stack}` }] };
228
+ }
229
+ }) as any);
230
+
231
+ const transport = new StdioServerTransport();
232
+ server.connect(transport);
233
+ try {
234
+ // Ensure the event loop stays active even if no client is attached yet
235
+ if (typeof process.stdin.resume === 'function') process.stdin.resume();
236
+ } catch {
237
+ /* ignore */
238
+ }
239
+
240
+ process.on('uncaughtException', (err) => {
241
+ const message = err instanceof Error ? `${err.message}\n${err.stack ?? ''}` : String(err);
242
+ try {
243
+ process.stderr.write(`[uncaughtException] ${message}\n`);
244
+ } catch {
245
+ /* ignore */
246
+ }
247
+ });
248
+
249
+ process.on('unhandledRejection', (reason) => {
250
+ const message = reason instanceof Error ? `${reason.message}\n${reason.stack ?? ''}` : String(reason);
251
+ try {
252
+ process.stderr.write(`[unhandledRejection] ${message}\n`);
253
+ } catch {
254
+ /* ignore */
255
+ }
256
+ });
257
+ };
258
+
259
+ // Allow running directly via ts-node/tsx
260
+ if (import.meta.url === `file://${process.argv[1]}`) {
261
+ // eslint-disable-next-line no-void
262
+ void startMcpServer();
263
+ }
@@ -0,0 +1,34 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
5
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
6
+
7
+ const here = path.dirname(fileURLToPath(import.meta.url));
8
+ const repoRoot = path.resolve(here, '../../../..');
9
+ const cliDist = path.resolve(repoRoot, 'packages/cli/dist/mcp.js');
10
+ const landing = path.resolve(repoRoot, 'packages/landing');
11
+
12
+ async function main() {
13
+ const transport = new StdioClientTransport({
14
+ command: 'node',
15
+ args: [cliDist],
16
+ cwd: landing,
17
+ env: {
18
+ ...process.env,
19
+ WORKSPACE_ROOT: landing,
20
+ },
21
+ stderr: 'pipe',
22
+ });
23
+ const client = new Client({ name: 'husar-mcp-smoke', version: '0.0.0' });
24
+ await client.connect(transport);
25
+
26
+ const res = await client.callTool({ name: 'husar_generate', arguments: {} });
27
+ const text = (res as any)?.content?.[0]?.text || '';
28
+ console.log('husar_generate result:', text);
29
+ await client.close();
30
+ }
31
+
32
+ // eslint-disable-next-line no-void
33
+ void main();
34
+