@hubspot/cli 7.7.27-experimental.2 → 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.
Files changed (130) hide show
  1. package/README.md +0 -4
  2. package/api/__tests__/migrate.test.js +5 -5
  3. package/api/migrate.d.ts +10 -4
  4. package/api/migrate.js +2 -2
  5. package/commands/__tests__/create.test.js +20 -0
  6. package/commands/__tests__/testAccount.test.js +2 -0
  7. package/commands/app/__tests__/migrate.test.js +1 -0
  8. package/commands/create/function.js +2 -2
  9. package/commands/create/module.js +2 -2
  10. package/commands/create/template.js +2 -2
  11. package/commands/create.js +47 -0
  12. package/commands/getStarted.js +66 -4
  13. package/commands/mcp/setup.d.ts +0 -1
  14. package/commands/mcp/setup.js +3 -11
  15. package/commands/project/__tests__/create.test.js +57 -0
  16. package/commands/project/__tests__/devUnifiedFlow.test.js +18 -30
  17. package/commands/project/create.js +6 -1
  18. package/commands/project/deploy.js +31 -1
  19. package/commands/project/dev/deprecatedFlow.js +2 -1
  20. package/commands/project/dev/index.js +32 -12
  21. package/commands/project/dev/unifiedFlow.d.ts +1 -1
  22. package/commands/project/dev/unifiedFlow.js +10 -16
  23. package/commands/project/profile/delete.js +26 -14
  24. package/commands/testAccount/__tests__/importData.test.d.ts +1 -0
  25. package/commands/testAccount/__tests__/importData.test.js +93 -0
  26. package/commands/testAccount/create.js +23 -13
  27. package/commands/testAccount/importData.d.ts +9 -0
  28. package/commands/testAccount/importData.js +61 -0
  29. package/commands/testAccount.js +2 -0
  30. package/lang/en.d.ts +162 -46
  31. package/lang/en.js +177 -59
  32. package/lang/en.lyaml +35 -14
  33. package/lib/__tests__/importData.test.d.ts +1 -0
  34. package/lib/__tests__/importData.test.js +89 -0
  35. package/lib/accountTypes.js +2 -3
  36. package/lib/app/__tests__/migrate.test.js +81 -36
  37. package/lib/app/migrate.d.ts +17 -4
  38. package/lib/app/migrate.js +97 -19
  39. package/lib/constants.d.ts +1 -0
  40. package/lib/constants.js +1 -0
  41. package/lib/hasFeature.d.ts +1 -0
  42. package/lib/hasFeature.js +7 -0
  43. package/lib/importData.d.ts +3 -0
  44. package/lib/importData.js +50 -0
  45. package/lib/mcp/setup.d.ts +3 -5
  46. package/lib/mcp/setup.js +39 -139
  47. package/lib/process.js +15 -4
  48. package/lib/projects/__tests__/AppDevModeInterface.test.js +3 -3
  49. package/lib/projects/__tests__/LocalDevProcess.test.js +5 -95
  50. package/lib/projects/__tests__/LocalDevWebsocketServer.test.js +6 -6
  51. package/lib/projects/__tests__/components.test.js +164 -7
  52. package/lib/projects/__tests__/localDevProjectHelpers.test.d.ts +1 -0
  53. package/lib/projects/__tests__/localDevProjectHelpers.test.js +118 -0
  54. package/lib/projects/add/v3AddComponent.js +16 -4
  55. package/lib/projects/components.d.ts +1 -0
  56. package/lib/projects/components.js +27 -1
  57. package/lib/projects/localDev/AppDevModeInterface.js +35 -3
  58. package/lib/projects/localDev/LocalDevLogger.d.ts +0 -4
  59. package/lib/projects/localDev/LocalDevLogger.js +2 -19
  60. package/lib/projects/localDev/LocalDevManager.js +1 -1
  61. package/lib/projects/localDev/LocalDevProcess.d.ts +1 -2
  62. package/lib/projects/localDev/LocalDevProcess.js +3 -26
  63. package/lib/projects/localDev/LocalDevState.d.ts +6 -7
  64. package/lib/projects/localDev/LocalDevState.js +16 -15
  65. package/lib/projects/localDev/LocalDevWebsocketServer.d.ts +1 -0
  66. package/lib/projects/localDev/LocalDevWebsocketServer.js +17 -2
  67. package/lib/projects/localDev/{helpers.d.ts → helpers/account.d.ts} +1 -7
  68. package/lib/projects/localDev/{helpers.js → helpers/account.js} +44 -144
  69. package/lib/projects/localDev/helpers/project.d.ts +12 -0
  70. package/lib/projects/localDev/helpers/project.js +173 -0
  71. package/lib/projects/urls.d.ts +1 -0
  72. package/lib/projects/urls.js +4 -0
  73. package/lib/prompts/__tests__/createFunctionPrompt.test.d.ts +1 -0
  74. package/lib/prompts/__tests__/createFunctionPrompt.test.js +129 -0
  75. package/lib/prompts/__tests__/createModulePrompt.test.d.ts +1 -0
  76. package/lib/prompts/__tests__/createModulePrompt.test.js +187 -0
  77. package/lib/prompts/__tests__/createTemplatePrompt.test.d.ts +1 -0
  78. package/lib/prompts/__tests__/createTemplatePrompt.test.js +102 -0
  79. package/lib/prompts/confirmImportDataPrompt.d.ts +1 -0
  80. package/lib/prompts/confirmImportDataPrompt.js +12 -0
  81. package/lib/prompts/createFunctionPrompt.d.ts +2 -1
  82. package/lib/prompts/createFunctionPrompt.js +36 -7
  83. package/lib/prompts/createModulePrompt.d.ts +2 -1
  84. package/lib/prompts/createModulePrompt.js +48 -1
  85. package/lib/prompts/createTemplatePrompt.d.ts +3 -24
  86. package/lib/prompts/createTemplatePrompt.js +9 -1
  87. package/lib/prompts/importDataFilePathPrompt.d.ts +1 -0
  88. package/lib/prompts/importDataFilePathPrompt.js +24 -0
  89. package/lib/prompts/importDataTestAccountSelectPrompt.d.ts +3 -0
  90. package/lib/prompts/importDataTestAccountSelectPrompt.js +29 -0
  91. package/lib/prompts/projectDevTargetAccountPrompt.js +1 -0
  92. package/lib/prompts/promptUtils.d.ts +7 -1
  93. package/lib/prompts/promptUtils.js +14 -1
  94. package/lib/ui/__tests__/removeAnsiCodes.test.d.ts +1 -0
  95. package/lib/ui/__tests__/removeAnsiCodes.test.js +84 -0
  96. package/lib/ui/index.js +3 -6
  97. package/lib/ui/removeAnsiCodes.d.ts +1 -0
  98. package/lib/ui/removeAnsiCodes.js +4 -0
  99. package/mcp-server/server.js +2 -1
  100. package/mcp-server/tools/cms/HsCreateModuleTool.d.ts +38 -0
  101. package/mcp-server/tools/cms/HsCreateModuleTool.js +118 -0
  102. package/mcp-server/tools/cms/HsListTool.d.ts +23 -0
  103. package/mcp-server/tools/cms/HsListTool.js +58 -0
  104. package/mcp-server/tools/cms/__tests__/HsCreateModuleTool.test.d.ts +1 -0
  105. package/mcp-server/tools/cms/__tests__/HsCreateModuleTool.test.js +224 -0
  106. package/mcp-server/tools/cms/__tests__/HsListTool.test.d.ts +1 -0
  107. package/mcp-server/tools/cms/__tests__/HsListTool.test.js +120 -0
  108. package/mcp-server/tools/index.d.ts +1 -0
  109. package/mcp-server/tools/index.js +12 -0
  110. package/mcp-server/tools/project/DocFetchTool.d.ts +17 -0
  111. package/mcp-server/tools/project/DocFetchTool.js +49 -0
  112. package/mcp-server/tools/project/DocsSearchTool.d.ts +26 -0
  113. package/mcp-server/tools/project/DocsSearchTool.js +62 -0
  114. package/mcp-server/tools/project/GetConfigValuesTool.js +3 -2
  115. package/mcp-server/tools/project/__tests__/DocFetchTool.test.d.ts +1 -0
  116. package/mcp-server/tools/project/__tests__/DocFetchTool.test.js +117 -0
  117. package/mcp-server/tools/project/__tests__/DocsSearchTool.test.d.ts +1 -0
  118. package/mcp-server/tools/project/__tests__/DocsSearchTool.test.js +190 -0
  119. package/mcp-server/tools/project/__tests__/GetConfigValuesTool.test.js +1 -1
  120. package/mcp-server/tools/project/constants.d.ts +2 -0
  121. package/mcp-server/tools/project/constants.js +6 -0
  122. package/mcp-server/utils/toolUsageTracking.d.ts +3 -1
  123. package/mcp-server/utils/toolUsageTracking.js +2 -1
  124. package/package.json +9 -6
  125. package/types/Cms.d.ts +16 -0
  126. package/types/Cms.js +25 -1
  127. package/types/LocalDev.d.ts +0 -3
  128. package/types/Prompts.d.ts +1 -0
  129. package/ui/index.d.ts +1 -0
  130. package/ui/index.js +6 -0
@@ -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
+ });
@@ -0,0 +1,120 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { HsListTool } from '../HsListTool.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('HsListTool', () => {
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 HsListTool(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('list-hubspot-cms-remote-contents', {
31
+ title: 'List HubSpot CMS Directory Contents',
32
+ description: 'List remote contents of a HubSpot CMS directory.',
33
+ inputSchema: expect.any(Object),
34
+ }, expect.any(Function));
35
+ expect(result).toBe(mockRegisteredTool);
36
+ });
37
+ });
38
+ describe('handler', () => {
39
+ it('should execute hs list command with no parameters', async () => {
40
+ mockRunCommandInDir.mockResolvedValue({
41
+ stdout: 'file1.html\nfile2.js\nfolder/',
42
+ stderr: '',
43
+ });
44
+ const result = await tool.handler({
45
+ absoluteCurrentWorkingDirectory: '/test/dir',
46
+ });
47
+ expect(mockRunCommandInDir).toHaveBeenCalledWith('/test/dir', 'hs list');
48
+ expect(result.content).toHaveLength(2);
49
+ expect(result.content[0].text).toContain('file1.html\nfile2.js\nfolder/');
50
+ expect(result.content[1].text).toBe('');
51
+ });
52
+ it('should execute hs list command with path parameter', async () => {
53
+ mockRunCommandInDir.mockResolvedValue({
54
+ stdout: 'nested-file.html',
55
+ stderr: '',
56
+ });
57
+ const result = await tool.handler({
58
+ absoluteCurrentWorkingDirectory: '/test/dir',
59
+ path: '/my-modules',
60
+ });
61
+ expect(mockRunCommandInDir).toHaveBeenCalledWith('/test/dir', 'hs list /my-modules');
62
+ expect(result.content).toHaveLength(2);
63
+ expect(result.content[0].text).toContain('nested-file.html');
64
+ expect(result.content[1].text).toBe('');
65
+ });
66
+ it('should execute hs list command with account parameter', async () => {
67
+ mockAddFlag.mockReturnValue('hs list --account test-account');
68
+ mockRunCommandInDir.mockResolvedValue({
69
+ stdout: 'account-specific-files.html',
70
+ stderr: '',
71
+ });
72
+ const result = await tool.handler({
73
+ absoluteCurrentWorkingDirectory: '/test/dir',
74
+ account: 'test-account',
75
+ });
76
+ expect(mockAddFlag).toHaveBeenCalledWith('hs list', 'account', 'test-account');
77
+ expect(mockRunCommandInDir).toHaveBeenCalledWith('/test/dir', 'hs list --account test-account');
78
+ expect(result.content).toHaveLength(2);
79
+ expect(result.content[0].text).toContain('account-specific-files.html');
80
+ expect(result.content[1].text).toBe('');
81
+ });
82
+ it('should execute hs list command with both path and account parameters', async () => {
83
+ mockAddFlag.mockReturnValue('hs list /my-path --account test-account');
84
+ mockRunCommandInDir.mockResolvedValue({
85
+ stdout: 'path-and-account-files.html',
86
+ stderr: '',
87
+ });
88
+ const result = await tool.handler({
89
+ absoluteCurrentWorkingDirectory: '/test/dir',
90
+ path: '/my-path',
91
+ account: 'test-account',
92
+ });
93
+ expect(mockAddFlag).toHaveBeenCalledWith('hs list /my-path', 'account', 'test-account');
94
+ expect(mockRunCommandInDir).toHaveBeenCalledWith('/test/dir', 'hs list /my-path --account test-account');
95
+ expect(result.content).toHaveLength(2);
96
+ expect(result.content[0].text).toContain('path-and-account-files.html');
97
+ expect(result.content[1].text).toBe('');
98
+ });
99
+ it('should handle command execution errors', async () => {
100
+ mockRunCommandInDir.mockRejectedValue(new Error('Command failed'));
101
+ const result = await tool.handler({
102
+ absoluteCurrentWorkingDirectory: '/test/dir',
103
+ });
104
+ expect(result.content).toHaveLength(1);
105
+ expect(result.content[0].text).toContain('Error executing hs list command: Command failed');
106
+ });
107
+ it('should handle stderr output', async () => {
108
+ mockRunCommandInDir.mockResolvedValue({
109
+ stdout: 'file1.html',
110
+ stderr: 'Warning: Some warning message',
111
+ });
112
+ const result = await tool.handler({
113
+ absoluteCurrentWorkingDirectory: '/test/dir',
114
+ });
115
+ expect(result.content).toHaveLength(2);
116
+ expect(result.content[0].text).toContain('file1.html');
117
+ expect(result.content[1].text).toContain('Warning: Some warning message');
118
+ });
119
+ });
120
+ });
@@ -1,2 +1,3 @@
1
1
  import { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  export declare function registerProjectTools(mcpServer: McpServer): RegisteredTool[];
3
+ export declare function registerCmsTools(mcpServer: McpServer): RegisteredTool[];
@@ -5,6 +5,10 @@ import { DeployProjectTool } from './project/DeployProjectTool.js';
5
5
  import { AddFeatureToProjectTool } from './project/AddFeatureToProjectTool.js';
6
6
  import { ValidateProjectTool } from './project/ValidateProjectTool.js';
7
7
  import { GetConfigValuesTool } from './project/GetConfigValuesTool.js';
8
+ import { DocsSearchTool } from './project/DocsSearchTool.js';
9
+ import { DocFetchTool } from './project/DocFetchTool.js';
10
+ import { HsListTool } from './cms/HsListTool.js';
11
+ import { HsCreateModuleTool } from './cms/HsCreateModuleTool.js';
8
12
  export function registerProjectTools(mcpServer) {
9
13
  return [
10
14
  new UploadProjectTools(mcpServer).register(),
@@ -14,5 +18,13 @@ export function registerProjectTools(mcpServer) {
14
18
  new AddFeatureToProjectTool(mcpServer).register(),
15
19
  new ValidateProjectTool(mcpServer).register(),
16
20
  new GetConfigValuesTool(mcpServer).register(),
21
+ new DocsSearchTool(mcpServer).register(),
22
+ new DocFetchTool(mcpServer).register(),
23
+ ];
24
+ }
25
+ export function registerCmsTools(mcpServer) {
26
+ return [
27
+ new HsListTool(mcpServer).register(),
28
+ new HsCreateModuleTool(mcpServer).register(),
17
29
  ];
18
30
  }
@@ -0,0 +1,17 @@
1
+ import { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import z from 'zod';
3
+ import { TextContentResponse, Tool } from '../../types.js';
4
+ declare const inputSchemaZodObject: z.ZodObject<{
5
+ docUrl: z.ZodString;
6
+ }, "strip", z.ZodTypeAny, {
7
+ docUrl: string;
8
+ }, {
9
+ docUrl: string;
10
+ }>;
11
+ type InputSchemaType = z.infer<typeof inputSchemaZodObject>;
12
+ export declare class DocFetchTool extends Tool<InputSchemaType> {
13
+ constructor(mcpServer: McpServer);
14
+ handler({ docUrl }: InputSchemaType): Promise<TextContentResponse>;
15
+ register(): RegisteredTool;
16
+ }
17
+ export {};
@@ -0,0 +1,49 @@
1
+ import z from 'zod';
2
+ import { Tool } from '../../types.js';
3
+ import { formatTextContents } from '../../utils/content.js';
4
+ import { trackToolUsage } from '../../utils/toolUsageTracking.js';
5
+ import { docUrl } from './constants.js';
6
+ import { http } from '@hubspot/local-dev-lib/http/unauthed';
7
+ import { isHubSpotHttpError } from '@hubspot/local-dev-lib/errors/index';
8
+ const inputSchema = {
9
+ docUrl,
10
+ };
11
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
12
+ const inputSchemaZodObject = z.object({
13
+ ...inputSchema,
14
+ });
15
+ const toolName = 'fetch-hubspot-doc';
16
+ export class DocFetchTool extends Tool {
17
+ constructor(mcpServer) {
18
+ super(mcpServer);
19
+ }
20
+ async handler({ docUrl }) {
21
+ await trackToolUsage(toolName);
22
+ try {
23
+ // Append .md extension to the URL
24
+ const markdownUrl = `${docUrl}.md`;
25
+ const response = await http.get({
26
+ url: markdownUrl,
27
+ });
28
+ const content = response.data;
29
+ if (!content || content.trim().length === 0) {
30
+ return formatTextContents('Document is empty or contains no content.');
31
+ }
32
+ return formatTextContents(content);
33
+ }
34
+ catch (error) {
35
+ if (isHubSpotHttpError(error)) {
36
+ return formatTextContents(error.toString());
37
+ }
38
+ const errorMessage = `Error fetching documentation: ${error instanceof Error ? error.message : String(error)}`;
39
+ return formatTextContents(errorMessage);
40
+ }
41
+ }
42
+ register() {
43
+ return this.mcpServer.registerTool(toolName, {
44
+ title: 'Fetch HubSpot Developer Documentation (single file)',
45
+ description: 'Always use this immediately after `search-hubspot-docs` and before creating a plan, writing code, or answering technical questions. This tool retrieves the full, authoritative content of a HubSpot Developer Documentation page from its URL, ensuring responses are accurate, up-to-date, and grounded in the official docs.',
46
+ inputSchema,
47
+ }, this.handler);
48
+ }
49
+ }
@@ -0,0 +1,26 @@
1
+ import { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import z from 'zod';
3
+ import { TextContentResponse, Tool } from '../../types.js';
4
+ declare const inputSchemaZodObject: z.ZodObject<{
5
+ docsSearchQuery: z.ZodString;
6
+ }, "strip", z.ZodTypeAny, {
7
+ docsSearchQuery: string;
8
+ }, {
9
+ docsSearchQuery: string;
10
+ }>;
11
+ export interface DocsSearchResponse {
12
+ results: {
13
+ title: string;
14
+ content: string;
15
+ description: string;
16
+ url: string;
17
+ score: number;
18
+ }[];
19
+ }
20
+ type InputSchemaType = z.infer<typeof inputSchemaZodObject>;
21
+ export declare class DocsSearchTool extends Tool<InputSchemaType> {
22
+ constructor(mcpServer: McpServer);
23
+ handler({ docsSearchQuery, }: InputSchemaType): Promise<TextContentResponse>;
24
+ register(): RegisteredTool;
25
+ }
26
+ export {};
@@ -0,0 +1,62 @@
1
+ import { http } from '@hubspot/local-dev-lib/http';
2
+ import z from 'zod';
3
+ import { Tool } from '../../types.js';
4
+ import { formatTextContents } from '../../utils/content.js';
5
+ import { trackToolUsage } from '../../utils/toolUsageTracking.js';
6
+ import { docsSearchQuery } from './constants.js';
7
+ import { getAccountId, getConfigPath, loadConfig, } from '@hubspot/local-dev-lib/config';
8
+ import { isHubSpotHttpError } from '@hubspot/local-dev-lib/errors/index';
9
+ const inputSchema = {
10
+ docsSearchQuery,
11
+ };
12
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
13
+ const inputSchemaZodObject = z.object({
14
+ ...inputSchema,
15
+ });
16
+ const toolName = 'search-hubspot-docs';
17
+ export class DocsSearchTool extends Tool {
18
+ constructor(mcpServer) {
19
+ super(mcpServer);
20
+ }
21
+ async handler({ docsSearchQuery, }) {
22
+ await trackToolUsage(toolName, { mode: docsSearchQuery });
23
+ loadConfig(getConfigPath());
24
+ const accountId = getAccountId();
25
+ if (!accountId) {
26
+ const authErrorMessage = `No account ID found. Please run \`hs account auth\` to configure an account, or set a default account with \`hs account use <account>\``;
27
+ return formatTextContents(authErrorMessage);
28
+ }
29
+ try {
30
+ const response = await http.post(accountId, {
31
+ url: 'dev/docs/llms/v1/docs-search',
32
+ data: {
33
+ query: docsSearchQuery,
34
+ },
35
+ });
36
+ const results = response.data.results;
37
+ if (!results || results.length === 0) {
38
+ return formatTextContents('No documentation found for your query.');
39
+ }
40
+ const formattedResults = results
41
+ .map(result => `**${result.title}**\n${result.description}\nURL: ${result.url}\nScore: ${result.score}\n\n${result.content}\n---\n`)
42
+ .join('\n');
43
+ const successMessage = `Found ${results.length} documentation results:\n\n${formattedResults}`;
44
+ return formatTextContents(successMessage);
45
+ }
46
+ catch (error) {
47
+ if (isHubSpotHttpError(error)) {
48
+ // Handle different status codes
49
+ return formatTextContents(error.toString());
50
+ }
51
+ const errorMessage = `Error searching documentation: ${error instanceof Error ? error.message : String(error)}`;
52
+ return formatTextContents(errorMessage);
53
+ }
54
+ }
55
+ register() {
56
+ return this.mcpServer.registerTool(toolName, {
57
+ title: 'Search HubSpot Developer Documentation',
58
+ description: 'Use this first whenever you need details about HubSpot APIs, SDKs, integrations, or developer platform features. This searches the official HubSpot Developer Documentation and returns the most relevant pages, each with a URL for use in `fetch-hubspot-doc`. Always follow this with a fetch to get the full, authoritative content before making plans or writing answers.',
59
+ inputSchema,
60
+ }, this.handler);
61
+ }
62
+ }
@@ -2,7 +2,7 @@ import { Tool } from '../../types.js';
2
2
  import { z } from 'zod';
3
3
  import { formatTextContents } from '../../utils/content.js';
4
4
  import { getIntermediateRepresentationSchema, mapToInternalType, } from '@hubspot/project-parsing-lib';
5
- import { getAccountId } from '@hubspot/local-dev-lib/config';
5
+ import { getAccountId, getConfigPath, loadConfig, } from '@hubspot/local-dev-lib/config';
6
6
  import { useV3Api } from '../../../lib/projects/buildAndDeploy.js';
7
7
  const inputSchema = {
8
8
  platformVersion: z
@@ -16,7 +16,7 @@ const inputSchema = {
16
16
  const inputSchemaZodObject = z.object({
17
17
  ...inputSchema,
18
18
  });
19
- const toolName = 'get-hubspot-project-feature-config-schema';
19
+ const toolName = 'get-hubspot-feature-config-schema';
20
20
  export class GetConfigValuesTool extends Tool {
21
21
  constructor(mcpServer) {
22
22
  super(mcpServer);
@@ -26,6 +26,7 @@ export class GetConfigValuesTool extends Tool {
26
26
  if (!useV3Api(platformVersion)) {
27
27
  return formatTextContents(`Can only be used on projects with a minimum platformVersion of 2025.2`);
28
28
  }
29
+ loadConfig(getConfigPath());
29
30
  const schema = await getIntermediateRepresentationSchema({
30
31
  platformVersion,
31
32
  projectSourceDir: '',