@hubspot/cli 7.7.28-experimental.0 → 7.7.29-experimental.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/commands/project/upload.d.ts +2 -2
- package/commands/project/upload.js +1 -1
- package/lang/en.d.ts +2 -0
- package/lang/en.js +2 -0
- package/lib/mcp/setup.d.ts +3 -3
- package/lib/mcp/setup.js +39 -115
- package/lib/projectProfiles.d.ts +1 -1
- package/lib/projectProfiles.js +2 -10
- package/mcp-server/tools/cms/HsCreateModuleTool.d.ts +38 -0
- package/mcp-server/tools/cms/HsCreateModuleTool.js +118 -0
- package/mcp-server/tools/cms/__tests__/HsCreateModuleTool.test.d.ts +1 -0
- package/mcp-server/tools/cms/__tests__/HsCreateModuleTool.test.js +224 -0
- package/mcp-server/tools/index.js +5 -1
- package/package.json +1 -1
- package/types/Yargs.d.ts +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { CommonArgs,
|
|
2
|
-
type ProjectUploadArgs = CommonArgs & JSONOutputArgs &
|
|
1
|
+
import { CommonArgs, JSONOutputArgs, YargsCommandModule } from '../../types/Yargs.js';
|
|
2
|
+
type ProjectUploadArgs = CommonArgs & JSONOutputArgs & {
|
|
3
3
|
forceCreate: boolean;
|
|
4
4
|
message: string;
|
|
5
5
|
m: string;
|
|
@@ -25,7 +25,7 @@ async function handler(args) {
|
|
|
25
25
|
validateProjectConfig(projectConfig, projectDir);
|
|
26
26
|
let targetAccountId;
|
|
27
27
|
if (useV3Api(projectConfig.platformVersion)) {
|
|
28
|
-
targetAccountId = await loadAndValidateProfile(projectConfig, projectDir, profile
|
|
28
|
+
targetAccountId = await loadAndValidateProfile(projectConfig, projectDir, profile);
|
|
29
29
|
}
|
|
30
30
|
targetAccountId = targetAccountId || derivedAccountId;
|
|
31
31
|
const accountConfig = getAccountConfig(targetAccountId);
|
package/lang/en.d.ts
CHANGED
|
@@ -884,10 +884,12 @@ Global configuration replaces hubspot.config.yml, and you will be prompted to mi
|
|
|
884
884
|
readonly configuringCursor: "Configuring Cursor...";
|
|
885
885
|
readonly failedToConfigureCursor: "Failed to configure Cursor";
|
|
886
886
|
readonly configuredCursor: "Configured Cursor";
|
|
887
|
+
readonly cursorNotFound: "Cursor not found - skipping configuration";
|
|
887
888
|
readonly alreadyInstalled: "HubSpot CLI mcp server already installed, reinstalling";
|
|
888
889
|
readonly configuringWindsurf: "Configuring Windsurf...";
|
|
889
890
|
readonly failedToConfigureWindsurf: "Failed to configure Windsurf";
|
|
890
891
|
readonly configuredWindsurf: "Configured Windsurf";
|
|
892
|
+
readonly windsurfNotFound: "Windsurf not found - skipping configuration";
|
|
891
893
|
readonly configuringVsCode: "Configuring VSCode...";
|
|
892
894
|
readonly failedToConfigureVsCode: "Failed to configure VSCode";
|
|
893
895
|
readonly configuredVsCode: "Configured VSCode";
|
package/lang/en.js
CHANGED
|
@@ -889,11 +889,13 @@ export const commands = {
|
|
|
889
889
|
configuringCursor: 'Configuring Cursor...',
|
|
890
890
|
failedToConfigureCursor: 'Failed to configure Cursor',
|
|
891
891
|
configuredCursor: 'Configured Cursor',
|
|
892
|
+
cursorNotFound: 'Cursor not found - skipping configuration',
|
|
892
893
|
alreadyInstalled: 'HubSpot CLI mcp server already installed, reinstalling',
|
|
893
894
|
// Windsurf
|
|
894
895
|
configuringWindsurf: 'Configuring Windsurf...',
|
|
895
896
|
failedToConfigureWindsurf: 'Failed to configure Windsurf',
|
|
896
897
|
configuredWindsurf: 'Configured Windsurf',
|
|
898
|
+
windsurfNotFound: 'Windsurf not found - skipping configuration',
|
|
897
899
|
// VS Code
|
|
898
900
|
configuringVsCode: 'Configuring VSCode...',
|
|
899
901
|
failedToConfigureVsCode: 'Failed to configure VSCode',
|
package/lib/mcp/setup.d.ts
CHANGED
|
@@ -16,8 +16,8 @@ interface McpCommand {
|
|
|
16
16
|
args: string[];
|
|
17
17
|
}
|
|
18
18
|
export declare function addMcpServerToConfig(targets: string[] | undefined): Promise<string[]>;
|
|
19
|
-
export declare function setupVsCode(mcpCommand?: McpCommand): Promise<boolean>;
|
|
20
19
|
export declare function setupClaudeCode(mcpCommand?: McpCommand): Promise<boolean>;
|
|
21
|
-
export declare function
|
|
22
|
-
export declare function
|
|
20
|
+
export declare function setupVsCode(mcpCommand?: McpCommand): Promise<boolean>;
|
|
21
|
+
export declare function setupCursor(mcpCommand?: McpCommand): Promise<boolean>;
|
|
22
|
+
export declare function setupWindsurf(mcpCommand?: McpCommand): Promise<boolean>;
|
|
23
23
|
export {};
|
package/lib/mcp/setup.js
CHANGED
|
@@ -4,10 +4,6 @@ import { promptUser } from '../prompts/promptUtils.js';
|
|
|
4
4
|
import SpinniesManager from '../ui/SpinniesManager.js';
|
|
5
5
|
import { logError } from '../errorHandlers/index.js';
|
|
6
6
|
import { execAsync } from '../../mcp-server/utils/command.js';
|
|
7
|
-
import path from 'path';
|
|
8
|
-
import os from 'os';
|
|
9
|
-
import fs from 'fs-extra';
|
|
10
|
-
import { existsSync } from 'fs';
|
|
11
7
|
const mcpServerName = 'hubspot-cli-mcp';
|
|
12
8
|
const claudeCode = 'claude';
|
|
13
9
|
const windsurf = 'windsurf';
|
|
@@ -72,96 +68,6 @@ async function runSetupFunction(func) {
|
|
|
72
68
|
throw new Error();
|
|
73
69
|
}
|
|
74
70
|
}
|
|
75
|
-
function setupMcpConfigFile(config) {
|
|
76
|
-
try {
|
|
77
|
-
SpinniesManager.add('spinner', {
|
|
78
|
-
text: config.configuringMessage,
|
|
79
|
-
});
|
|
80
|
-
if (!existsSync(config.configPath)) {
|
|
81
|
-
fs.writeFileSync(config.configPath, JSON.stringify({}, null, 2));
|
|
82
|
-
}
|
|
83
|
-
let mcpConfig = {};
|
|
84
|
-
let configContent;
|
|
85
|
-
try {
|
|
86
|
-
configContent = fs.readFileSync(config.configPath, 'utf8');
|
|
87
|
-
}
|
|
88
|
-
catch (error) {
|
|
89
|
-
SpinniesManager.fail('spinner', {
|
|
90
|
-
text: config.failedMessage,
|
|
91
|
-
});
|
|
92
|
-
logError(error);
|
|
93
|
-
return false;
|
|
94
|
-
}
|
|
95
|
-
try {
|
|
96
|
-
// In the event the file exists, but is empty, initialize it to and empty object
|
|
97
|
-
if (configContent.trim() === '') {
|
|
98
|
-
mcpConfig = {};
|
|
99
|
-
}
|
|
100
|
-
else {
|
|
101
|
-
mcpConfig = JSON.parse(configContent);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
catch (error) {
|
|
105
|
-
SpinniesManager.fail('spinner', {
|
|
106
|
-
text: config.failedMessage,
|
|
107
|
-
});
|
|
108
|
-
uiLogger.error(commands.mcp.setup.errors.errorParsingJsonFIle(config.configPath, error instanceof Error ? error.message : `${error}`));
|
|
109
|
-
return false;
|
|
110
|
-
}
|
|
111
|
-
// Initialize mcpServers if it doesn't exist
|
|
112
|
-
if (!mcpConfig.mcpServers) {
|
|
113
|
-
mcpConfig.mcpServers = {};
|
|
114
|
-
}
|
|
115
|
-
// Add or update HubSpot CLI MCP server
|
|
116
|
-
mcpConfig.mcpServers[mcpServerName] = {
|
|
117
|
-
...config.mcpCommand,
|
|
118
|
-
};
|
|
119
|
-
// Write the updated config
|
|
120
|
-
fs.writeFileSync(config.configPath, JSON.stringify(mcpConfig, null, 2));
|
|
121
|
-
SpinniesManager.succeed('spinner', {
|
|
122
|
-
text: config.configuredMessage,
|
|
123
|
-
});
|
|
124
|
-
return true;
|
|
125
|
-
}
|
|
126
|
-
catch (error) {
|
|
127
|
-
SpinniesManager.fail('spinner', {
|
|
128
|
-
text: config.failedMessage,
|
|
129
|
-
});
|
|
130
|
-
logError(error);
|
|
131
|
-
return false;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
export async function setupVsCode(mcpCommand = defaultMcpCommand) {
|
|
135
|
-
try {
|
|
136
|
-
SpinniesManager.add('vsCode', {
|
|
137
|
-
text: commands.mcp.setup.spinners.configuringVsCode,
|
|
138
|
-
});
|
|
139
|
-
const mcpConfig = JSON.stringify({
|
|
140
|
-
name: mcpServerName,
|
|
141
|
-
...buildCommandWithAgentString(mcpCommand, vscode),
|
|
142
|
-
});
|
|
143
|
-
await execAsync(`code --add-mcp '${mcpConfig}'`);
|
|
144
|
-
SpinniesManager.succeed('vsCode', {
|
|
145
|
-
text: commands.mcp.setup.spinners.configuredVsCode,
|
|
146
|
-
});
|
|
147
|
-
return true;
|
|
148
|
-
}
|
|
149
|
-
catch (error) {
|
|
150
|
-
if (error instanceof Error &&
|
|
151
|
-
error.message.includes('code: command not found')) {
|
|
152
|
-
SpinniesManager.fail('vsCode', {
|
|
153
|
-
text: commands.mcp.setup.spinners.vsCodeNotFound,
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
else {
|
|
157
|
-
SpinniesManager.fail('vsCode', {
|
|
158
|
-
text: commands.mcp.setup.spinners.failedToConfigureVsCode,
|
|
159
|
-
});
|
|
160
|
-
logError(error);
|
|
161
|
-
}
|
|
162
|
-
return false;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
71
|
export async function setupClaudeCode(mcpCommand = defaultMcpCommand) {
|
|
166
72
|
try {
|
|
167
73
|
SpinniesManager.add('claudeCode', {
|
|
@@ -182,15 +88,14 @@ export async function setupClaudeCode(mcpCommand = defaultMcpCommand) {
|
|
|
182
88
|
});
|
|
183
89
|
await execAsync(`claude mcp remove "${mcpServerName}" --scope user`);
|
|
184
90
|
}
|
|
185
|
-
await execAsync(`claude mcp add-json "${mcpServerName}"
|
|
91
|
+
await execAsync(`claude mcp add-json "${mcpServerName}" ${JSON.stringify(mcpConfig)} --scope user`);
|
|
186
92
|
SpinniesManager.succeed('claudeCode', {
|
|
187
93
|
text: commands.mcp.setup.spinners.configuredClaudeCode,
|
|
188
94
|
});
|
|
189
95
|
return true;
|
|
190
96
|
}
|
|
191
97
|
catch (error) {
|
|
192
|
-
if (error instanceof Error &&
|
|
193
|
-
error.message.includes('claude: command not found')) {
|
|
98
|
+
if (error instanceof Error && error.message.includes('claude')) {
|
|
194
99
|
SpinniesManager.fail('claudeCode', {
|
|
195
100
|
text: commands.mcp.setup.spinners.claudeCodeNotFound,
|
|
196
101
|
});
|
|
@@ -212,25 +117,44 @@ export async function setupClaudeCode(mcpCommand = defaultMcpCommand) {
|
|
|
212
117
|
return false;
|
|
213
118
|
}
|
|
214
119
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
120
|
+
async function setupVsCodeBasedIntegration(commandName, configuringText, configuredText, notFoundText, failedText, mcpCommand = defaultMcpCommand) {
|
|
121
|
+
try {
|
|
122
|
+
SpinniesManager.add(commandName, {
|
|
123
|
+
text: configuringText,
|
|
124
|
+
});
|
|
125
|
+
const mcpConfig = JSON.stringify({
|
|
126
|
+
name: mcpServerName,
|
|
127
|
+
...buildCommandWithAgentString(mcpCommand, vscode),
|
|
128
|
+
});
|
|
129
|
+
await execAsync(`${commandName} --add-mcp ${JSON.stringify(mcpConfig)}`);
|
|
130
|
+
SpinniesManager.succeed(commandName, {
|
|
131
|
+
text: configuredText,
|
|
132
|
+
});
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
if (error instanceof Error && error.message.includes(commandName)) {
|
|
137
|
+
SpinniesManager.fail(commandName, {
|
|
138
|
+
text: notFoundText,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
SpinniesManager.fail(commandName, {
|
|
143
|
+
text: failedText,
|
|
144
|
+
});
|
|
145
|
+
logError(error);
|
|
146
|
+
}
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
export async function setupVsCode(mcpCommand = defaultMcpCommand) {
|
|
151
|
+
return setupVsCodeBasedIntegration('code', commands.mcp.setup.spinners.configuringVsCode, commands.mcp.setup.spinners.configuredVsCode, commands.mcp.setup.spinners.vsCodeNotFound, commands.mcp.setup.spinners.failedToConfigureVsCode, mcpCommand);
|
|
152
|
+
}
|
|
153
|
+
export async function setupCursor(mcpCommand = defaultMcpCommand) {
|
|
154
|
+
return setupVsCodeBasedIntegration('cursor', commands.mcp.setup.spinners.configuringCursor, commands.mcp.setup.spinners.configuredCursor, commands.mcp.setup.spinners.cursorNotFound, commands.mcp.setup.spinners.failedToConfigureCursor, mcpCommand);
|
|
224
155
|
}
|
|
225
|
-
export function setupWindsurf(mcpCommand = defaultMcpCommand) {
|
|
226
|
-
|
|
227
|
-
return setupMcpConfigFile({
|
|
228
|
-
configPath: windsurfConfigPath,
|
|
229
|
-
configuringMessage: commands.mcp.setup.spinners.configuringWindsurf,
|
|
230
|
-
configuredMessage: commands.mcp.setup.spinners.configuredWindsurf,
|
|
231
|
-
failedMessage: commands.mcp.setup.spinners.failedToConfigureWindsurf,
|
|
232
|
-
mcpCommand: buildCommandWithAgentString(mcpCommand, windsurf),
|
|
233
|
-
});
|
|
156
|
+
export async function setupWindsurf(mcpCommand = defaultMcpCommand) {
|
|
157
|
+
return setupVsCodeBasedIntegration('windsurf', commands.mcp.setup.spinners.configuringWindsurf, commands.mcp.setup.spinners.configuredWindsurf, commands.mcp.setup.spinners.windsurfNotFound, commands.mcp.setup.spinners.failedToConfigureWindsurf, mcpCommand);
|
|
234
158
|
}
|
|
235
159
|
function buildCommandWithAgentString(mcpCommand, agent) {
|
|
236
160
|
const mcpCommandCopy = structuredClone(mcpCommand);
|
package/lib/projectProfiles.d.ts
CHANGED
|
@@ -4,4 +4,4 @@ export declare function logProfileHeader(profileName: string): void;
|
|
|
4
4
|
export declare function logProfileFooter(profile: HsProfileFile, includeVariables?: boolean): void;
|
|
5
5
|
export declare function loadProfile(projectConfig: ProjectConfig | null, projectDir: string | null, profileName: string): HsProfileFile | undefined;
|
|
6
6
|
export declare function exitIfUsingProfiles(projectConfig: ProjectConfig | null, projectDir: string | null): Promise<void>;
|
|
7
|
-
export declare function loadAndValidateProfile(projectConfig: ProjectConfig | null, projectDir: string | null, argsProfile: string | undefined
|
|
7
|
+
export declare function loadAndValidateProfile(projectConfig: ProjectConfig | null, projectDir: string | null, argsProfile: string | undefined): Promise<number | undefined>;
|
package/lib/projectProfiles.js
CHANGED
|
@@ -38,12 +38,7 @@ export function loadProfile(projectConfig, projectDir, profileName) {
|
|
|
38
38
|
uiLogger.error(lib.projectProfiles.loadProfile.errors.missingAccountId(profileFilename));
|
|
39
39
|
return;
|
|
40
40
|
}
|
|
41
|
-
return
|
|
42
|
-
...profile,
|
|
43
|
-
accountId: process.env.HUBSPOT_ACCOUNT_ID
|
|
44
|
-
? Number(process.env.HUBSPOT_ACCOUNT_ID)
|
|
45
|
-
: profile.accountId,
|
|
46
|
-
};
|
|
41
|
+
return profile;
|
|
47
42
|
}
|
|
48
43
|
catch (e) {
|
|
49
44
|
uiLogger.error(lib.projectProfiles.loadProfile.errors.failedToLoadProfile(profileFilename));
|
|
@@ -59,7 +54,7 @@ export async function exitIfUsingProfiles(projectConfig, projectDir) {
|
|
|
59
54
|
}
|
|
60
55
|
}
|
|
61
56
|
}
|
|
62
|
-
export async function loadAndValidateProfile(projectConfig, projectDir, argsProfile
|
|
57
|
+
export async function loadAndValidateProfile(projectConfig, projectDir, argsProfile) {
|
|
63
58
|
if (argsProfile) {
|
|
64
59
|
logProfileHeader(argsProfile);
|
|
65
60
|
const profile = loadProfile(projectConfig, projectDir, argsProfile);
|
|
@@ -68,9 +63,6 @@ export async function loadAndValidateProfile(projectConfig, projectDir, argsProf
|
|
|
68
63
|
process.exit(EXIT_CODES.ERROR);
|
|
69
64
|
}
|
|
70
65
|
logProfileFooter(profile, true);
|
|
71
|
-
if (useEnv) {
|
|
72
|
-
return Number(process.env.HUBSPOT_ACCOUNT_ID);
|
|
73
|
-
}
|
|
74
66
|
return profile.accountId;
|
|
75
67
|
}
|
|
76
68
|
else {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { TextContentResponse, Tool } from '../../types.js';
|
|
2
|
+
import { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
declare const inputSchemaZodObject: z.ZodObject<{
|
|
5
|
+
absoluteCurrentWorkingDirectory: z.ZodString;
|
|
6
|
+
userSuppliedName: z.ZodOptional<z.ZodString>;
|
|
7
|
+
dest: z.ZodOptional<z.ZodString>;
|
|
8
|
+
moduleLabel: z.ZodOptional<z.ZodString>;
|
|
9
|
+
reactType: z.ZodOptional<z.ZodBoolean>;
|
|
10
|
+
contentTypes: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
11
|
+
global: z.ZodOptional<z.ZodBoolean>;
|
|
12
|
+
availableForNewContent: z.ZodOptional<z.ZodBoolean>;
|
|
13
|
+
}, "strip", z.ZodTypeAny, {
|
|
14
|
+
absoluteCurrentWorkingDirectory: string;
|
|
15
|
+
dest?: string | undefined;
|
|
16
|
+
global?: boolean | undefined;
|
|
17
|
+
moduleLabel?: string | undefined;
|
|
18
|
+
reactType?: boolean | undefined;
|
|
19
|
+
contentTypes?: string | undefined;
|
|
20
|
+
availableForNewContent?: boolean | undefined;
|
|
21
|
+
userSuppliedName?: string | undefined;
|
|
22
|
+
}, {
|
|
23
|
+
absoluteCurrentWorkingDirectory: string;
|
|
24
|
+
dest?: string | undefined;
|
|
25
|
+
global?: boolean | undefined;
|
|
26
|
+
moduleLabel?: string | undefined;
|
|
27
|
+
reactType?: boolean | undefined;
|
|
28
|
+
contentTypes?: string | undefined;
|
|
29
|
+
availableForNewContent?: boolean | undefined;
|
|
30
|
+
userSuppliedName?: string | undefined;
|
|
31
|
+
}>;
|
|
32
|
+
export type HsCreateModuleInputSchema = z.infer<typeof inputSchemaZodObject>;
|
|
33
|
+
export declare class HsCreateModuleTool extends Tool<HsCreateModuleInputSchema> {
|
|
34
|
+
constructor(mcpServer: McpServer);
|
|
35
|
+
handler({ userSuppliedName, dest, moduleLabel, reactType, contentTypes, global, availableForNewContent, absoluteCurrentWorkingDirectory, }: HsCreateModuleInputSchema): Promise<TextContentResponse>;
|
|
36
|
+
register(): RegisteredTool;
|
|
37
|
+
}
|
|
38
|
+
export {};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { Tool } from '../../types.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { absoluteCurrentWorkingDirectory } from '../project/constants.js';
|
|
4
|
+
import { runCommandInDir } from '../../utils/project.js';
|
|
5
|
+
import { formatTextContents, formatTextContent } from '../../utils/content.js';
|
|
6
|
+
import { trackToolUsage } from '../../utils/toolUsageTracking.js';
|
|
7
|
+
import { addFlag } from '../../utils/command.js';
|
|
8
|
+
import { CONTENT_TYPES } from '../../../types/Cms.js';
|
|
9
|
+
const inputSchema = {
|
|
10
|
+
absoluteCurrentWorkingDirectory,
|
|
11
|
+
userSuppliedName: z
|
|
12
|
+
.string()
|
|
13
|
+
.describe('REQUIRED - If not specified by the user, DO NOT choose. Ask the user to specify the name of the module they want to create.')
|
|
14
|
+
.optional(),
|
|
15
|
+
dest: z
|
|
16
|
+
.string()
|
|
17
|
+
.describe('The destination path where the module should be created on the current computer.')
|
|
18
|
+
.optional(),
|
|
19
|
+
moduleLabel: z
|
|
20
|
+
.string()
|
|
21
|
+
.describe('Label for module creation. Required for non-interactive module creation. If not provided, ask the user to provide it.')
|
|
22
|
+
.optional(),
|
|
23
|
+
reactType: z
|
|
24
|
+
.boolean()
|
|
25
|
+
.describe('Whether to create a React module. If the user has not specified that they want a React module, DO NOT choose for them, ask them what type of module they want to create HubL or React.')
|
|
26
|
+
.optional(),
|
|
27
|
+
contentTypes: z
|
|
28
|
+
.string()
|
|
29
|
+
.refine(val => {
|
|
30
|
+
if (!val)
|
|
31
|
+
return true; // optional
|
|
32
|
+
const types = val.split(',').map(t => t.trim().toUpperCase());
|
|
33
|
+
return types.every(type => CONTENT_TYPES.includes(type));
|
|
34
|
+
}, {
|
|
35
|
+
message: `Content types must be a comma-separated list of valid values: ${CONTENT_TYPES.join(', ')}`,
|
|
36
|
+
})
|
|
37
|
+
.describe(`Content types where the module can be used. Comma-separated list. Valid values: ${CONTENT_TYPES.join(', ')}. Defaults to "ANY".`)
|
|
38
|
+
.optional(),
|
|
39
|
+
global: z
|
|
40
|
+
.boolean()
|
|
41
|
+
.describe('Whether the module is global. Defaults to false.')
|
|
42
|
+
.optional(),
|
|
43
|
+
availableForNewContent: z
|
|
44
|
+
.boolean()
|
|
45
|
+
.describe('Whether the module is available for new content. Defaults to true.')
|
|
46
|
+
.optional(),
|
|
47
|
+
};
|
|
48
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
49
|
+
const inputSchemaZodObject = z.object({ ...inputSchema });
|
|
50
|
+
const toolName = 'create-hubspot-cms-module';
|
|
51
|
+
export class HsCreateModuleTool extends Tool {
|
|
52
|
+
constructor(mcpServer) {
|
|
53
|
+
super(mcpServer);
|
|
54
|
+
}
|
|
55
|
+
async handler({ userSuppliedName, dest, moduleLabel, reactType, contentTypes, global, availableForNewContent, absoluteCurrentWorkingDirectory, }) {
|
|
56
|
+
await trackToolUsage(toolName);
|
|
57
|
+
const content = [];
|
|
58
|
+
// Always require a name
|
|
59
|
+
if (!userSuppliedName) {
|
|
60
|
+
content.push(formatTextContent(`Ask the user to specify the name of the module they want to create.`));
|
|
61
|
+
}
|
|
62
|
+
// Require module label
|
|
63
|
+
if (!moduleLabel) {
|
|
64
|
+
content.push(formatTextContent(`Ask the user to provide a label for the module.`));
|
|
65
|
+
}
|
|
66
|
+
// Ask about React vs HubL if not specified
|
|
67
|
+
if (reactType === undefined) {
|
|
68
|
+
content.push(formatTextContent(`Ask the user what type of module they want to create: HubL or React?`));
|
|
69
|
+
}
|
|
70
|
+
// If we have missing required information, return the prompts
|
|
71
|
+
if (content.length > 0) {
|
|
72
|
+
return {
|
|
73
|
+
content,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
// Build the command
|
|
77
|
+
let command = 'hs create module';
|
|
78
|
+
if (userSuppliedName) {
|
|
79
|
+
command += ` "${userSuppliedName}"`;
|
|
80
|
+
}
|
|
81
|
+
if (dest) {
|
|
82
|
+
command += ` "${dest}"`;
|
|
83
|
+
}
|
|
84
|
+
// Add module-specific flags
|
|
85
|
+
if (moduleLabel) {
|
|
86
|
+
command = addFlag(command, 'module-label', moduleLabel);
|
|
87
|
+
}
|
|
88
|
+
if (reactType !== undefined) {
|
|
89
|
+
command = addFlag(command, 'react-type', reactType);
|
|
90
|
+
}
|
|
91
|
+
if (contentTypes) {
|
|
92
|
+
command = addFlag(command, 'content-types', contentTypes);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
command = addFlag(command, 'content-types', 'ANY');
|
|
96
|
+
}
|
|
97
|
+
if (global !== undefined) {
|
|
98
|
+
command = addFlag(command, 'global', global);
|
|
99
|
+
}
|
|
100
|
+
if (availableForNewContent !== undefined) {
|
|
101
|
+
command = addFlag(command, 'available-for-new-content', availableForNewContent);
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
const { stdout, stderr } = await runCommandInDir(absoluteCurrentWorkingDirectory, command);
|
|
105
|
+
return formatTextContents(stdout, stderr);
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
return formatTextContents(error instanceof Error ? error.message : `${error}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
register() {
|
|
112
|
+
return this.mcpServer.registerTool(toolName, {
|
|
113
|
+
title: 'Create HubSpot CMS Module',
|
|
114
|
+
description: 'Creates a new HubSpot CMS module using the hs create module command. Modules can be created non-interactively by specifying moduleLabel and other module options. You can create either HubL or React modules by setting the reactType parameter.',
|
|
115
|
+
inputSchema,
|
|
116
|
+
}, this.handler);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { HsCreateModuleTool } from '../HsCreateModuleTool.js';
|
|
3
|
+
import { runCommandInDir } from '../../../utils/project.js';
|
|
4
|
+
import { addFlag } from '../../../utils/command.js';
|
|
5
|
+
vi.mock('@modelcontextprotocol/sdk/server/mcp.js');
|
|
6
|
+
vi.mock('../../../utils/project');
|
|
7
|
+
vi.mock('../../../utils/command');
|
|
8
|
+
vi.mock('../../../utils/toolUsageTracking', () => ({
|
|
9
|
+
trackToolUsage: vi.fn(),
|
|
10
|
+
}));
|
|
11
|
+
const mockRunCommandInDir = runCommandInDir;
|
|
12
|
+
const mockAddFlag = addFlag;
|
|
13
|
+
describe('HsCreateModuleTool', () => {
|
|
14
|
+
let mockMcpServer;
|
|
15
|
+
let tool;
|
|
16
|
+
let mockRegisteredTool;
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
vi.clearAllMocks();
|
|
19
|
+
// @ts-expect-error Not mocking the whole server
|
|
20
|
+
mockMcpServer = {
|
|
21
|
+
registerTool: vi.fn(),
|
|
22
|
+
};
|
|
23
|
+
mockRegisteredTool = {};
|
|
24
|
+
mockMcpServer.registerTool.mockReturnValue(mockRegisteredTool);
|
|
25
|
+
tool = new HsCreateModuleTool(mockMcpServer);
|
|
26
|
+
});
|
|
27
|
+
describe('register', () => {
|
|
28
|
+
it('should register the tool with the MCP server', () => {
|
|
29
|
+
const result = tool.register();
|
|
30
|
+
expect(mockMcpServer.registerTool).toHaveBeenCalledWith('create-hubspot-cms-module', {
|
|
31
|
+
title: 'Create HubSpot CMS Module',
|
|
32
|
+
description: 'Creates a new HubSpot CMS module using the hs create module command. Modules can be created non-interactively by specifying moduleLabel and other module options. You can create either HubL or React modules by setting the reactType parameter.',
|
|
33
|
+
inputSchema: expect.any(Object),
|
|
34
|
+
}, expect.any(Function));
|
|
35
|
+
expect(result).toBe(mockRegisteredTool);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
describe('handler', () => {
|
|
39
|
+
it('should prompt for missing required parameters', async () => {
|
|
40
|
+
const result = await tool.handler({
|
|
41
|
+
absoluteCurrentWorkingDirectory: '/test/dir',
|
|
42
|
+
});
|
|
43
|
+
expect(result.content).toHaveLength(3);
|
|
44
|
+
expect(result.content[0].text).toContain('Ask the user to specify the name of the module');
|
|
45
|
+
expect(result.content[1].text).toContain('Ask the user to provide a label for the module');
|
|
46
|
+
expect(result.content[2].text).toContain('Ask the user what type of module they want to create: HubL or React?');
|
|
47
|
+
});
|
|
48
|
+
it('should prompt for missing name only when other params provided', async () => {
|
|
49
|
+
const result = await tool.handler({
|
|
50
|
+
absoluteCurrentWorkingDirectory: '/test/dir',
|
|
51
|
+
moduleLabel: 'Test Label',
|
|
52
|
+
reactType: false,
|
|
53
|
+
});
|
|
54
|
+
expect(result.content).toHaveLength(1);
|
|
55
|
+
expect(result.content[0].text).toContain('Ask the user to specify the name of the module');
|
|
56
|
+
});
|
|
57
|
+
it('should prompt for missing moduleLabel when name provided', async () => {
|
|
58
|
+
const result = await tool.handler({
|
|
59
|
+
absoluteCurrentWorkingDirectory: '/test/dir',
|
|
60
|
+
userSuppliedName: 'Test Module',
|
|
61
|
+
reactType: true,
|
|
62
|
+
});
|
|
63
|
+
expect(result.content).toHaveLength(1);
|
|
64
|
+
expect(result.content[0].text).toContain('Ask the user to provide a label for the module');
|
|
65
|
+
});
|
|
66
|
+
it('should prompt for missing reactType when other required params provided', async () => {
|
|
67
|
+
const result = await tool.handler({
|
|
68
|
+
absoluteCurrentWorkingDirectory: '/test/dir',
|
|
69
|
+
userSuppliedName: 'Test Module',
|
|
70
|
+
moduleLabel: 'Test Label',
|
|
71
|
+
});
|
|
72
|
+
expect(result.content).toHaveLength(1);
|
|
73
|
+
expect(result.content[0].text).toContain('Ask the user what type of module they want to create: HubL or React?');
|
|
74
|
+
});
|
|
75
|
+
it('should execute command with all required parameters (HubL module)', async () => {
|
|
76
|
+
mockAddFlag
|
|
77
|
+
.mockReturnValueOnce('hs create module "Test Module" --module-label Test Label')
|
|
78
|
+
.mockReturnValueOnce('hs create module "Test Module" --module-label Test Label --react-type false')
|
|
79
|
+
.mockReturnValueOnce('hs create module "Test Module" --module-label Test Label --react-type false --content-types ANY');
|
|
80
|
+
mockRunCommandInDir.mockResolvedValue({
|
|
81
|
+
stdout: 'Module created successfully',
|
|
82
|
+
stderr: '',
|
|
83
|
+
});
|
|
84
|
+
const result = await tool.handler({
|
|
85
|
+
absoluteCurrentWorkingDirectory: '/test/dir',
|
|
86
|
+
userSuppliedName: 'Test Module',
|
|
87
|
+
moduleLabel: 'Test Label',
|
|
88
|
+
reactType: false,
|
|
89
|
+
});
|
|
90
|
+
expect(mockAddFlag).toHaveBeenCalledWith('hs create module "Test Module"', 'module-label', 'Test Label');
|
|
91
|
+
expect(mockAddFlag).toHaveBeenCalledWith(expect.stringContaining('module-label'), 'react-type', false);
|
|
92
|
+
expect(mockAddFlag).toHaveBeenCalledWith(expect.stringContaining('react-type'), 'content-types', 'ANY');
|
|
93
|
+
expect(mockRunCommandInDir).toHaveBeenCalledWith('/test/dir', expect.stringContaining('hs create module'));
|
|
94
|
+
expect(result.content).toHaveLength(2);
|
|
95
|
+
expect(result.content[0].text).toContain('Module created successfully');
|
|
96
|
+
});
|
|
97
|
+
it('should execute command with React module', async () => {
|
|
98
|
+
mockAddFlag
|
|
99
|
+
.mockReturnValueOnce('hs create module "React Module" --module-label React Label')
|
|
100
|
+
.mockReturnValueOnce('hs create module "React Module" --module-label React Label --react-type true')
|
|
101
|
+
.mockReturnValueOnce('hs create module "React Module" --module-label React Label --react-type true --content-types ANY');
|
|
102
|
+
mockRunCommandInDir.mockResolvedValue({
|
|
103
|
+
stdout: 'React module created successfully',
|
|
104
|
+
stderr: '',
|
|
105
|
+
});
|
|
106
|
+
const result = await tool.handler({
|
|
107
|
+
absoluteCurrentWorkingDirectory: '/test/dir',
|
|
108
|
+
userSuppliedName: 'React Module',
|
|
109
|
+
moduleLabel: 'React Label',
|
|
110
|
+
reactType: true,
|
|
111
|
+
});
|
|
112
|
+
expect(mockAddFlag).toHaveBeenCalledWith(expect.stringContaining('module-label'), 'react-type', true);
|
|
113
|
+
expect(result.content[0].text).toContain('React module created successfully');
|
|
114
|
+
});
|
|
115
|
+
it('should execute command with destination path', async () => {
|
|
116
|
+
mockAddFlag
|
|
117
|
+
.mockReturnValueOnce('hs create module "Test Module" "custom/path" --module-label Test Label')
|
|
118
|
+
.mockReturnValueOnce('hs create module "Test Module" "custom/path" --module-label Test Label --react-type false')
|
|
119
|
+
.mockReturnValueOnce('hs create module "Test Module" "custom/path" --module-label Test Label --react-type false --content-types ANY');
|
|
120
|
+
mockRunCommandInDir.mockResolvedValue({
|
|
121
|
+
stdout: 'Module created at custom path',
|
|
122
|
+
stderr: '',
|
|
123
|
+
});
|
|
124
|
+
const result = await tool.handler({
|
|
125
|
+
absoluteCurrentWorkingDirectory: '/test/dir',
|
|
126
|
+
userSuppliedName: 'Test Module',
|
|
127
|
+
dest: 'custom/path',
|
|
128
|
+
moduleLabel: 'Test Label',
|
|
129
|
+
reactType: false,
|
|
130
|
+
});
|
|
131
|
+
expect(mockRunCommandInDir).toHaveBeenCalledWith('/test/dir', expect.stringContaining('"custom/path"'));
|
|
132
|
+
expect(result.content[0].text).toContain('Module created at custom path');
|
|
133
|
+
});
|
|
134
|
+
it('should execute command with custom content types', async () => {
|
|
135
|
+
mockAddFlag
|
|
136
|
+
.mockReturnValueOnce('hs create module "Test Module" --module-label Test Label')
|
|
137
|
+
.mockReturnValueOnce('hs create module "Test Module" --module-label Test Label --react-type false')
|
|
138
|
+
.mockReturnValueOnce('hs create module "Test Module" --module-label Test Label --react-type false --content-types LANDING_PAGE,BLOG_POST');
|
|
139
|
+
mockRunCommandInDir.mockResolvedValue({
|
|
140
|
+
stdout: 'Module with custom content types created',
|
|
141
|
+
stderr: '',
|
|
142
|
+
});
|
|
143
|
+
const result = await tool.handler({
|
|
144
|
+
absoluteCurrentWorkingDirectory: '/test/dir',
|
|
145
|
+
userSuppliedName: 'Test Module',
|
|
146
|
+
moduleLabel: 'Test Label',
|
|
147
|
+
reactType: false,
|
|
148
|
+
contentTypes: 'LANDING_PAGE,BLOG_POST',
|
|
149
|
+
});
|
|
150
|
+
expect(mockAddFlag).toHaveBeenCalledWith(expect.stringContaining('react-type'), 'content-types', 'LANDING_PAGE,BLOG_POST');
|
|
151
|
+
expect(result.content[0].text).toContain('Module with custom content types created');
|
|
152
|
+
});
|
|
153
|
+
it('should execute command with global flag', async () => {
|
|
154
|
+
mockAddFlag
|
|
155
|
+
.mockReturnValueOnce('hs create module "Global Module" --module-label Global Label')
|
|
156
|
+
.mockReturnValueOnce('hs create module "Global Module" --module-label Global Label --react-type false')
|
|
157
|
+
.mockReturnValueOnce('hs create module "Global Module" --module-label Global Label --react-type false --content-types ANY')
|
|
158
|
+
.mockReturnValueOnce('hs create module "Global Module" --module-label Global Label --react-type false --content-types ANY --global true');
|
|
159
|
+
mockRunCommandInDir.mockResolvedValue({
|
|
160
|
+
stdout: 'Global module created',
|
|
161
|
+
stderr: '',
|
|
162
|
+
});
|
|
163
|
+
const result = await tool.handler({
|
|
164
|
+
absoluteCurrentWorkingDirectory: '/test/dir',
|
|
165
|
+
userSuppliedName: 'Global Module',
|
|
166
|
+
moduleLabel: 'Global Label',
|
|
167
|
+
reactType: false,
|
|
168
|
+
global: true,
|
|
169
|
+
});
|
|
170
|
+
expect(mockAddFlag).toHaveBeenCalledWith(expect.stringContaining('content-types'), 'global', true);
|
|
171
|
+
expect(result.content[0].text).toContain('Global module created');
|
|
172
|
+
});
|
|
173
|
+
it('should execute command with availableForNewContent flag', async () => {
|
|
174
|
+
mockAddFlag
|
|
175
|
+
.mockReturnValueOnce('hs create module "Test Module" --module-label Test Label')
|
|
176
|
+
.mockReturnValueOnce('hs create module "Test Module" --module-label Test Label --react-type false')
|
|
177
|
+
.mockReturnValueOnce('hs create module "Test Module" --module-label Test Label --react-type false --content-types ANY')
|
|
178
|
+
.mockReturnValueOnce('hs create module "Test Module" --module-label Test Label --react-type false --content-types ANY --available-for-new-content false');
|
|
179
|
+
mockRunCommandInDir.mockResolvedValue({
|
|
180
|
+
stdout: 'Module created with availableForNewContent false',
|
|
181
|
+
stderr: '',
|
|
182
|
+
});
|
|
183
|
+
const result = await tool.handler({
|
|
184
|
+
absoluteCurrentWorkingDirectory: '/test/dir',
|
|
185
|
+
userSuppliedName: 'Test Module',
|
|
186
|
+
moduleLabel: 'Test Label',
|
|
187
|
+
reactType: false,
|
|
188
|
+
availableForNewContent: false,
|
|
189
|
+
});
|
|
190
|
+
expect(mockAddFlag).toHaveBeenCalledWith(expect.stringContaining('content-types'), 'available-for-new-content', false);
|
|
191
|
+
expect(result.content[0].text).toContain('Module created with availableForNewContent false');
|
|
192
|
+
});
|
|
193
|
+
it('should handle command execution errors', async () => {
|
|
194
|
+
mockRunCommandInDir.mockRejectedValue(new Error('Command failed'));
|
|
195
|
+
const result = await tool.handler({
|
|
196
|
+
absoluteCurrentWorkingDirectory: '/test/dir',
|
|
197
|
+
userSuppliedName: 'Test Module',
|
|
198
|
+
moduleLabel: 'Test Label',
|
|
199
|
+
reactType: false,
|
|
200
|
+
});
|
|
201
|
+
expect(result.content).toHaveLength(1);
|
|
202
|
+
expect(result.content[0].text).toContain('Command failed');
|
|
203
|
+
});
|
|
204
|
+
it('should handle stderr output', async () => {
|
|
205
|
+
mockAddFlag
|
|
206
|
+
.mockReturnValueOnce('hs create module "Test Module" --module-label Test Label')
|
|
207
|
+
.mockReturnValueOnce('hs create module "Test Module" --module-label Test Label --react-type false')
|
|
208
|
+
.mockReturnValueOnce('hs create module "Test Module" --module-label Test Label --react-type false --content-types ANY');
|
|
209
|
+
mockRunCommandInDir.mockResolvedValue({
|
|
210
|
+
stdout: 'Module created successfully',
|
|
211
|
+
stderr: 'Warning: Deprecated feature used',
|
|
212
|
+
});
|
|
213
|
+
const result = await tool.handler({
|
|
214
|
+
absoluteCurrentWorkingDirectory: '/test/dir',
|
|
215
|
+
userSuppliedName: 'Test Module',
|
|
216
|
+
moduleLabel: 'Test Label',
|
|
217
|
+
reactType: false,
|
|
218
|
+
});
|
|
219
|
+
expect(result.content).toHaveLength(2);
|
|
220
|
+
expect(result.content[0].text).toContain('Module created successfully');
|
|
221
|
+
expect(result.content[1].text).toContain('Warning: Deprecated feature used');
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
});
|
|
@@ -8,6 +8,7 @@ import { GetConfigValuesTool } from './project/GetConfigValuesTool.js';
|
|
|
8
8
|
import { DocsSearchTool } from './project/DocsSearchTool.js';
|
|
9
9
|
import { DocFetchTool } from './project/DocFetchTool.js';
|
|
10
10
|
import { HsListTool } from './cms/HsListTool.js';
|
|
11
|
+
import { HsCreateModuleTool } from './cms/HsCreateModuleTool.js';
|
|
11
12
|
export function registerProjectTools(mcpServer) {
|
|
12
13
|
return [
|
|
13
14
|
new UploadProjectTools(mcpServer).register(),
|
|
@@ -22,5 +23,8 @@ export function registerProjectTools(mcpServer) {
|
|
|
22
23
|
];
|
|
23
24
|
}
|
|
24
25
|
export function registerCmsTools(mcpServer) {
|
|
25
|
-
return [
|
|
26
|
+
return [
|
|
27
|
+
new HsListTool(mcpServer).register(),
|
|
28
|
+
new HsCreateModuleTool(mcpServer).register(),
|
|
29
|
+
];
|
|
26
30
|
}
|
package/package.json
CHANGED