@husar.ai/cli 0.2.11 → 0.2.13
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 +84 -0
- package/dist/cli.js +14 -49
- package/dist/cli.js.map +1 -1
- package/dist/functions/generate.d.ts +7 -0
- package/dist/functions/generate.js +46 -0
- package/dist/functions/generate.js.map +1 -0
- package/dist/mcp.d.ts +2 -0
- package/dist/mcp.js +217 -0
- package/dist/mcp.js.map +1 -0
- package/dist/mcp.test.d.ts +1 -0
- package/dist/mcp.test.js +27 -0
- package/dist/mcp.test.js.map +1 -0
- package/package.json +6 -3
- package/src/cli.ts +18 -54
- package/src/functions/generate.ts +66 -0
- package/src/mcp.test.ts +31 -0
- package/src/mcp.ts +256 -0
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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":";AAGA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
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,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
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
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 { parser } from './functions/parser.js';
|
|
9
|
+
import { generateCms } from './functions/generate.js';
|
|
10
|
+
const server = new McpServer({ name: 'mcp-husar', version: '1.0.0' });
|
|
11
|
+
let config;
|
|
12
|
+
const getWorkspaceRoot = async () => {
|
|
13
|
+
const envRoot = process.env.WORKSPACE_ROOT;
|
|
14
|
+
if (envRoot)
|
|
15
|
+
return envRoot;
|
|
16
|
+
let cur = process.cwd();
|
|
17
|
+
for (let i = 0; i < 10; i++) {
|
|
18
|
+
try {
|
|
19
|
+
const pkgPath = path.join(cur, 'package.json');
|
|
20
|
+
const st = await fs.stat(pkgPath);
|
|
21
|
+
if (st.isFile()) {
|
|
22
|
+
const txt = await fs.readFile(pkgPath, 'utf8');
|
|
23
|
+
const pkg = JSON.parse(txt);
|
|
24
|
+
if (pkg && Object.prototype.hasOwnProperty.call(pkg, 'workspaces'))
|
|
25
|
+
return cur;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
}
|
|
30
|
+
const parent = path.dirname(cur);
|
|
31
|
+
if (parent === cur)
|
|
32
|
+
break;
|
|
33
|
+
cur = parent;
|
|
34
|
+
}
|
|
35
|
+
return process.cwd();
|
|
36
|
+
};
|
|
37
|
+
const resolveWorkspacePath = async (p) => {
|
|
38
|
+
if (path.isAbsolute(p))
|
|
39
|
+
return p;
|
|
40
|
+
const root = await getWorkspaceRoot();
|
|
41
|
+
return path.resolve(root, p);
|
|
42
|
+
};
|
|
43
|
+
const getConfig = async () => {
|
|
44
|
+
if (config)
|
|
45
|
+
return config;
|
|
46
|
+
const root = await getWorkspaceRoot();
|
|
47
|
+
config = new ConfigMaker('husar', {
|
|
48
|
+
decoders: {},
|
|
49
|
+
pathToProject: root,
|
|
50
|
+
});
|
|
51
|
+
return config;
|
|
52
|
+
};
|
|
53
|
+
const getAuth = async () => {
|
|
54
|
+
const envHost = process.env.HUSAR_MCP_HOST;
|
|
55
|
+
const envToken = process.env.HUSAR_MCP_ADMIN_TOKEN;
|
|
56
|
+
if (envHost && envToken)
|
|
57
|
+
return { host: envHost, adminToken: envToken };
|
|
58
|
+
try {
|
|
59
|
+
const cfg = await getConfig();
|
|
60
|
+
const { host, adminToken } = cfg.get();
|
|
61
|
+
if (host && adminToken)
|
|
62
|
+
return { host, adminToken };
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
}
|
|
66
|
+
throw new Error('Missing HUSAR_MCP_HOST and/or HUSAR_MCP_ADMIN_TOKEN. Provide via env or husar.json in workspace root. ' +
|
|
67
|
+
(await getWorkspaceRoot()));
|
|
68
|
+
};
|
|
69
|
+
const cmsParseFileInputFields = {
|
|
70
|
+
path: z
|
|
71
|
+
.string()
|
|
72
|
+
.min(1)
|
|
73
|
+
.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'),
|
|
74
|
+
type: z
|
|
75
|
+
.enum(['model', 'shape'])
|
|
76
|
+
.optional()
|
|
77
|
+
.default('shape')
|
|
78
|
+
.describe('Target in CMS. Use "shape" for UI/component structures (default), or "model" for data models.'),
|
|
79
|
+
name: z
|
|
80
|
+
.string()
|
|
81
|
+
.optional()
|
|
82
|
+
.describe('Optional CMS name. Must not contain dashes (-). Prefer lowercase with underscores (e.g., examples_contactform). Defaults to the filename (lowercased).'),
|
|
83
|
+
};
|
|
84
|
+
const cmsParseFileInputSchema = z.object(cmsParseFileInputFields);
|
|
85
|
+
const withTimeout = async (promise, ms) => {
|
|
86
|
+
let timeoutId;
|
|
87
|
+
try {
|
|
88
|
+
return await Promise.race([
|
|
89
|
+
promise,
|
|
90
|
+
new Promise((_, reject) => {
|
|
91
|
+
timeoutId = setTimeout(() => reject(new Error(`Operation timed out after ${ms} ms`)), ms);
|
|
92
|
+
}),
|
|
93
|
+
]);
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
if (timeoutId)
|
|
97
|
+
clearTimeout(timeoutId);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
const redirectConsoleToStderr = () => {
|
|
101
|
+
const original = {
|
|
102
|
+
log: console.log,
|
|
103
|
+
info: console.info,
|
|
104
|
+
debug: console.debug,
|
|
105
|
+
dir: console.dir,
|
|
106
|
+
};
|
|
107
|
+
console.log = (...args) => process.stderr.write(args.map(String).join(' ') + '\n');
|
|
108
|
+
console.info = (...args) => process.stderr.write(args.map(String).join(' ') + '\n');
|
|
109
|
+
console.debug = (...args) => process.stderr.write(args.map(String).join(' ') + '\n');
|
|
110
|
+
console.dir = (obj) => {
|
|
111
|
+
try {
|
|
112
|
+
const text = typeof obj === 'string' ? obj : JSON.stringify(obj, null, 2);
|
|
113
|
+
process.stderr.write(text + '\n');
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
process.stderr.write(String(obj) + '\n');
|
|
117
|
+
}
|
|
118
|
+
return obj;
|
|
119
|
+
};
|
|
120
|
+
return () => {
|
|
121
|
+
console.log = original.log;
|
|
122
|
+
console.info = original.info;
|
|
123
|
+
console.debug = original.debug;
|
|
124
|
+
console.dir = original.dir;
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
const createCmsParseFileHandler = ({ timeoutMs = 30_000 }) => {
|
|
128
|
+
return async ({ path: file, type, name: passed }) => {
|
|
129
|
+
const restoreConsole = redirectConsoleToStderr();
|
|
130
|
+
try {
|
|
131
|
+
const { host, adminToken } = await getAuth();
|
|
132
|
+
const absPath = await resolveWorkspacePath(file);
|
|
133
|
+
const name = (passed ?? (absPath.replace(/^.*\/(.*?)(\.[^.]+)?$/, '$1') || 'untitled')).toLowerCase();
|
|
134
|
+
const result = await withTimeout(parser(absPath, { type: (type ?? 'shape'), name }, {
|
|
135
|
+
HUSAR_MCP_HOST: host,
|
|
136
|
+
HUSAR_MCP_ADMIN_TOKEN: adminToken,
|
|
137
|
+
}), timeoutMs);
|
|
138
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
142
|
+
const stack = err instanceof Error && err.stack ? `\nStack: ${err.stack}` : '';
|
|
143
|
+
return { isError: true, content: [{ type: 'text', text: `Error: ${message}${stack}` }] };
|
|
144
|
+
}
|
|
145
|
+
finally {
|
|
146
|
+
try {
|
|
147
|
+
restoreConsole();
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
export const startMcpServer = async () => {
|
|
155
|
+
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) }));
|
|
156
|
+
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) }));
|
|
157
|
+
const husarGenerateInput = {
|
|
158
|
+
folderPath: z
|
|
159
|
+
.string()
|
|
160
|
+
.optional()
|
|
161
|
+
.describe('Target folder for cms scaffolding. Relative to workspace root; defaults to ".".'),
|
|
162
|
+
};
|
|
163
|
+
const husarGenerateSchema = z.object(husarGenerateInput);
|
|
164
|
+
server.tool('husar_generate', 'Generate cms structure in the given path', husarGenerateInput, (async (args) => {
|
|
165
|
+
try {
|
|
166
|
+
const { folderPath } = husarGenerateSchema.parse(args ?? {});
|
|
167
|
+
const root = await getWorkspaceRoot();
|
|
168
|
+
const cfg = await getConfig();
|
|
169
|
+
const raw = cfg.get();
|
|
170
|
+
const host = raw.host || process.env.HUSAR_MCP_HOST || process.env.HUSAR_HOST;
|
|
171
|
+
if (!host) {
|
|
172
|
+
throw new Error('Missing host. Provide via husar.json (host) or HUSAR_MCP_HOST/HUSAR_HOST env.');
|
|
173
|
+
}
|
|
174
|
+
const base = folderPath ? await resolveWorkspacePath(folderPath) : root;
|
|
175
|
+
const cmsPath = await generateCms({
|
|
176
|
+
baseFolder: base,
|
|
177
|
+
host,
|
|
178
|
+
hostEnvironmentVariable: raw.hostEnvironmentVariable,
|
|
179
|
+
authenticationEnvironmentVariable: raw.authenticationEnvironmentVariable,
|
|
180
|
+
});
|
|
181
|
+
return { content: [{ type: 'text', text: JSON.stringify({ status: 'ok', folder: cmsPath }, null, 2) }] };
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
185
|
+
const stack = err instanceof Error && err.stack ? `\nStack: ${err.stack}` : '';
|
|
186
|
+
return { isError: true, content: [{ type: 'text', text: `Error: ${message}${stack}` }] };
|
|
187
|
+
}
|
|
188
|
+
}));
|
|
189
|
+
const transport = new StdioServerTransport();
|
|
190
|
+
server.connect(transport);
|
|
191
|
+
try {
|
|
192
|
+
if (typeof process.stdin.resume === 'function')
|
|
193
|
+
process.stdin.resume();
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
}
|
|
197
|
+
process.on('uncaughtException', (err) => {
|
|
198
|
+
const message = err instanceof Error ? `${err.message}\n${err.stack ?? ''}` : String(err);
|
|
199
|
+
try {
|
|
200
|
+
process.stderr.write(`[uncaughtException] ${message}\n`);
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
process.on('unhandledRejection', (reason) => {
|
|
206
|
+
const message = reason instanceof Error ? `${reason.message}\n${reason.stack ?? ''}` : String(reason);
|
|
207
|
+
try {
|
|
208
|
+
process.stderr.write(`[unhandledRejection] ${message}\n`);
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
};
|
|
214
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
215
|
+
void startMcpServer();
|
|
216
|
+
}
|
|
217
|
+
//# sourceMappingURL=mcp.js.map
|
package/dist/mcp.js.map
ADDED
|
@@ -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 {};
|
package/dist/mcp.test.js
ADDED
|
@@ -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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@husar.ai/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.13",
|
|
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.
|
|
19
|
+
"@husar.ai/ssr": "^0.2.13",
|
|
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
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
+
|
package/src/mcp.test.ts
ADDED
|
@@ -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,256 @@
|
|
|
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 { parser } from './functions/parser.js';
|
|
10
|
+
import { generateCms } from './functions/generate.js';
|
|
11
|
+
|
|
12
|
+
const server = new McpServer({ name: 'mcp-husar', version: '1.0.0' });
|
|
13
|
+
|
|
14
|
+
let config: ConfigMaker<{ host: string; adminToken: string }> | undefined;
|
|
15
|
+
|
|
16
|
+
const getWorkspaceRoot = async (): Promise<string> => {
|
|
17
|
+
const envRoot = process.env.WORKSPACE_ROOT;
|
|
18
|
+
if (envRoot) return envRoot;
|
|
19
|
+
let cur = process.cwd();
|
|
20
|
+
for (let i = 0; i < 10; i++) {
|
|
21
|
+
try {
|
|
22
|
+
const pkgPath = path.join(cur, 'package.json');
|
|
23
|
+
const st = await fs.stat(pkgPath);
|
|
24
|
+
if (st.isFile()) {
|
|
25
|
+
const txt = await fs.readFile(pkgPath, 'utf8');
|
|
26
|
+
const pkg = JSON.parse(txt) as { workspaces?: unknown };
|
|
27
|
+
if (pkg && Object.prototype.hasOwnProperty.call(pkg, 'workspaces')) return cur;
|
|
28
|
+
}
|
|
29
|
+
} catch {
|
|
30
|
+
/* empty */
|
|
31
|
+
}
|
|
32
|
+
const parent = path.dirname(cur);
|
|
33
|
+
if (parent === cur) break;
|
|
34
|
+
cur = parent;
|
|
35
|
+
}
|
|
36
|
+
return process.cwd();
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const resolveWorkspacePath = async (p: string): Promise<string> => {
|
|
40
|
+
if (path.isAbsolute(p)) return p;
|
|
41
|
+
const root = await getWorkspaceRoot();
|
|
42
|
+
return path.resolve(root, p);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const getConfig = async () => {
|
|
46
|
+
if (config) return config;
|
|
47
|
+
const root = await getWorkspaceRoot();
|
|
48
|
+
config = new ConfigMaker<{ host: string; adminToken: string }>('husar', {
|
|
49
|
+
decoders: {},
|
|
50
|
+
pathToProject: root,
|
|
51
|
+
});
|
|
52
|
+
return config;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const getAuth = async () => {
|
|
56
|
+
const envHost = process.env.HUSAR_MCP_HOST;
|
|
57
|
+
const envToken = process.env.HUSAR_MCP_ADMIN_TOKEN;
|
|
58
|
+
if (envHost && envToken) return { host: envHost, adminToken: envToken } as const;
|
|
59
|
+
|
|
60
|
+
// Non-interactive: read current config values only
|
|
61
|
+
try {
|
|
62
|
+
const cfg = await getConfig();
|
|
63
|
+
const { host, adminToken } = cfg.get();
|
|
64
|
+
if (host && adminToken) return { host, adminToken } as const;
|
|
65
|
+
} catch {
|
|
66
|
+
// ignore; we'll throw below
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
throw new Error(
|
|
70
|
+
'Missing HUSAR_MCP_HOST and/or HUSAR_MCP_ADMIN_TOKEN. Provide via env or husar.json in workspace root. ' +
|
|
71
|
+
(await getWorkspaceRoot()),
|
|
72
|
+
);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const cmsParseFileInputFields = {
|
|
76
|
+
path: z
|
|
77
|
+
.string()
|
|
78
|
+
.min(1)
|
|
79
|
+
.describe(
|
|
80
|
+
'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',
|
|
81
|
+
),
|
|
82
|
+
type: z
|
|
83
|
+
.enum(['model', 'shape'])
|
|
84
|
+
.optional()
|
|
85
|
+
.default('shape')
|
|
86
|
+
.describe('Target in CMS. Use "shape" for UI/component structures (default), or "model" for data models.'),
|
|
87
|
+
name: z
|
|
88
|
+
.string()
|
|
89
|
+
.optional()
|
|
90
|
+
.describe(
|
|
91
|
+
'Optional CMS name. Must not contain dashes (-). Prefer lowercase with underscores (e.g., examples_contactform). Defaults to the filename (lowercased).',
|
|
92
|
+
),
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const cmsParseFileInputSchema = z.object(cmsParseFileInputFields);
|
|
96
|
+
type CmsParseFileInput = z.infer<typeof cmsParseFileInputSchema>;
|
|
97
|
+
|
|
98
|
+
const withTimeout = async <T>(promise: Promise<T>, ms: number): Promise<T> => {
|
|
99
|
+
let timeoutId: NodeJS.Timeout | undefined;
|
|
100
|
+
try {
|
|
101
|
+
return await Promise.race([
|
|
102
|
+
promise,
|
|
103
|
+
new Promise<T>((_, reject) => {
|
|
104
|
+
timeoutId = setTimeout(() => reject(new Error(`Operation timed out after ${ms} ms`)), ms);
|
|
105
|
+
}),
|
|
106
|
+
]);
|
|
107
|
+
} finally {
|
|
108
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const redirectConsoleToStderr = () => {
|
|
113
|
+
const original = {
|
|
114
|
+
log: console.log,
|
|
115
|
+
info: console.info,
|
|
116
|
+
debug: console.debug,
|
|
117
|
+
dir: console.dir,
|
|
118
|
+
} as const;
|
|
119
|
+
console.log = (...args: unknown[]) => process.stderr.write(args.map(String).join(' ') + '\n');
|
|
120
|
+
console.info = (...args: unknown[]) => process.stderr.write(args.map(String).join(' ') + '\n');
|
|
121
|
+
console.debug = (...args: unknown[]) => process.stderr.write(args.map(String).join(' ') + '\n');
|
|
122
|
+
console.dir = (obj: unknown) => {
|
|
123
|
+
try {
|
|
124
|
+
const text = typeof obj === 'string' ? obj : JSON.stringify(obj, null, 2);
|
|
125
|
+
process.stderr.write(text + '\n');
|
|
126
|
+
} catch {
|
|
127
|
+
process.stderr.write(String(obj) + '\n');
|
|
128
|
+
}
|
|
129
|
+
return obj as never;
|
|
130
|
+
};
|
|
131
|
+
return () => {
|
|
132
|
+
console.log = original.log;
|
|
133
|
+
console.info = original.info;
|
|
134
|
+
console.debug = original.debug;
|
|
135
|
+
console.dir = original.dir;
|
|
136
|
+
};
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const createCmsParseFileHandler = ({ timeoutMs = 30_000 }: { timeoutMs?: number }) => {
|
|
140
|
+
return async ({ path: file, type, name: passed }: CmsParseFileInput) => {
|
|
141
|
+
const restoreConsole = redirectConsoleToStderr();
|
|
142
|
+
try {
|
|
143
|
+
const { host, adminToken } = await getAuth();
|
|
144
|
+
const absPath = await resolveWorkspacePath(file);
|
|
145
|
+
const name = (passed ?? (absPath.replace(/^.*\/(.*?)(\.[^.]+)?$/, '$1') || 'untitled')).toLowerCase();
|
|
146
|
+
const result = await withTimeout(
|
|
147
|
+
parser(
|
|
148
|
+
absPath,
|
|
149
|
+
{ type: (type ?? 'shape') as 'model' | 'shape', name },
|
|
150
|
+
{
|
|
151
|
+
HUSAR_MCP_HOST: host,
|
|
152
|
+
HUSAR_MCP_ADMIN_TOKEN: adminToken,
|
|
153
|
+
},
|
|
154
|
+
),
|
|
155
|
+
timeoutMs,
|
|
156
|
+
);
|
|
157
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
158
|
+
} catch (err) {
|
|
159
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
160
|
+
const stack = err instanceof Error && err.stack ? `\nStack: ${err.stack}` : '';
|
|
161
|
+
return { isError: true, content: [{ type: 'text', text: `Error: ${message}${stack}` }] };
|
|
162
|
+
} finally {
|
|
163
|
+
try {
|
|
164
|
+
restoreConsole();
|
|
165
|
+
} catch {
|
|
166
|
+
/* no-op */
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
export const startMcpServer = async () => {
|
|
173
|
+
// Tool: cms_parse_file (legacy)
|
|
174
|
+
server.tool(
|
|
175
|
+
'cms_parse_file',
|
|
176
|
+
'Parse a CMS file into a shape or model',
|
|
177
|
+
cmsParseFileInputFields,
|
|
178
|
+
createCmsParseFileHandler({ timeoutMs: Number(process.env.MCP_PARSER_TIMEOUT_MS || 30000) }) as any,
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
// Tool: husar_copy (alias)
|
|
182
|
+
server.tool(
|
|
183
|
+
'husar_copy',
|
|
184
|
+
'Copy: parse HTML/JSX and upsert into CMS (model/shape)',
|
|
185
|
+
cmsParseFileInputFields,
|
|
186
|
+
createCmsParseFileHandler({ timeoutMs: Number(process.env.MCP_PARSER_TIMEOUT_MS || 30000) }) as any,
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
// Tool: husar_generate
|
|
190
|
+
const husarGenerateInput = {
|
|
191
|
+
folderPath: z
|
|
192
|
+
.string()
|
|
193
|
+
.optional()
|
|
194
|
+
.describe('Target folder for cms scaffolding. Relative to workspace root; defaults to ".".'),
|
|
195
|
+
} as const;
|
|
196
|
+
const husarGenerateSchema = z.object(husarGenerateInput);
|
|
197
|
+
server.tool('husar_generate', 'Generate cms structure in the given path', husarGenerateInput, (async (
|
|
198
|
+
args: unknown,
|
|
199
|
+
) => {
|
|
200
|
+
try {
|
|
201
|
+
const { folderPath } = husarGenerateSchema.parse(args ?? {});
|
|
202
|
+
const root = await getWorkspaceRoot();
|
|
203
|
+
const cfg = await getConfig();
|
|
204
|
+
const raw = cfg.get();
|
|
205
|
+
const host = raw.host || process.env.HUSAR_MCP_HOST || process.env.HUSAR_HOST;
|
|
206
|
+
if (!host) {
|
|
207
|
+
throw new Error('Missing host. Provide via husar.json (host) or HUSAR_MCP_HOST/HUSAR_HOST env.');
|
|
208
|
+
}
|
|
209
|
+
const base = folderPath ? await resolveWorkspacePath(folderPath) : root;
|
|
210
|
+
const cmsPath = await generateCms({
|
|
211
|
+
baseFolder: base,
|
|
212
|
+
host,
|
|
213
|
+
hostEnvironmentVariable: (raw as any).hostEnvironmentVariable,
|
|
214
|
+
authenticationEnvironmentVariable: (raw as any).authenticationEnvironmentVariable,
|
|
215
|
+
});
|
|
216
|
+
return { content: [{ type: 'text', text: JSON.stringify({ status: 'ok', folder: cmsPath }, null, 2) }] };
|
|
217
|
+
} catch (err) {
|
|
218
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
219
|
+
const stack = err instanceof Error && err.stack ? `\nStack: ${err.stack}` : '';
|
|
220
|
+
return { isError: true, content: [{ type: 'text', text: `Error: ${message}${stack}` }] };
|
|
221
|
+
}
|
|
222
|
+
}) as any);
|
|
223
|
+
|
|
224
|
+
const transport = new StdioServerTransport();
|
|
225
|
+
server.connect(transport);
|
|
226
|
+
try {
|
|
227
|
+
// Ensure the event loop stays active even if no client is attached yet
|
|
228
|
+
if (typeof process.stdin.resume === 'function') process.stdin.resume();
|
|
229
|
+
} catch {
|
|
230
|
+
/* ignore */
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
process.on('uncaughtException', (err) => {
|
|
234
|
+
const message = err instanceof Error ? `${err.message}\n${err.stack ?? ''}` : String(err);
|
|
235
|
+
try {
|
|
236
|
+
process.stderr.write(`[uncaughtException] ${message}\n`);
|
|
237
|
+
} catch {
|
|
238
|
+
/* ignore */
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
process.on('unhandledRejection', (reason) => {
|
|
243
|
+
const message = reason instanceof Error ? `${reason.message}\n${reason.stack ?? ''}` : String(reason);
|
|
244
|
+
try {
|
|
245
|
+
process.stderr.write(`[unhandledRejection] ${message}\n`);
|
|
246
|
+
} catch {
|
|
247
|
+
/* ignore */
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
// Allow running directly via ts-node/tsx
|
|
253
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
254
|
+
// eslint-disable-next-line no-void
|
|
255
|
+
void startMcpServer();
|
|
256
|
+
}
|