@hubspot/cli 7.7.27-experimental.2 → 7.7.28-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/README.md +0 -4
- package/api/__tests__/migrate.test.js +5 -5
- package/api/migrate.d.ts +10 -4
- package/api/migrate.js +2 -2
- package/commands/__tests__/create.test.js +20 -0
- package/commands/__tests__/testAccount.test.js +2 -0
- package/commands/app/__tests__/migrate.test.js +1 -0
- package/commands/create/function.js +2 -2
- package/commands/create/module.js +2 -2
- package/commands/create/template.js +2 -2
- package/commands/create.js +47 -0
- package/commands/getStarted.js +66 -4
- package/commands/mcp/setup.d.ts +0 -1
- package/commands/mcp/setup.js +3 -11
- package/commands/project/__tests__/create.test.js +57 -0
- package/commands/project/__tests__/devUnifiedFlow.test.js +18 -30
- package/commands/project/create.js +6 -1
- package/commands/project/deploy.js +31 -1
- package/commands/project/dev/deprecatedFlow.js +2 -1
- package/commands/project/dev/index.js +32 -12
- package/commands/project/dev/unifiedFlow.d.ts +1 -1
- package/commands/project/dev/unifiedFlow.js +10 -16
- package/commands/project/profile/delete.js +26 -14
- package/commands/project/upload.d.ts +2 -2
- package/commands/project/upload.js +1 -1
- package/commands/testAccount/__tests__/importData.test.d.ts +1 -0
- package/commands/testAccount/__tests__/importData.test.js +93 -0
- package/commands/testAccount/create.js +23 -13
- package/commands/testAccount/importData.d.ts +9 -0
- package/commands/testAccount/importData.js +61 -0
- package/commands/testAccount.js +2 -0
- package/lang/en.d.ts +160 -46
- package/lang/en.js +175 -59
- package/lang/en.lyaml +35 -14
- package/lib/__tests__/importData.test.d.ts +1 -0
- package/lib/__tests__/importData.test.js +89 -0
- package/lib/accountTypes.js +2 -3
- package/lib/app/__tests__/migrate.test.js +81 -36
- package/lib/app/migrate.d.ts +17 -4
- package/lib/app/migrate.js +97 -19
- package/lib/constants.d.ts +1 -0
- package/lib/constants.js +1 -0
- package/lib/hasFeature.d.ts +1 -0
- package/lib/hasFeature.js +7 -0
- package/lib/importData.d.ts +3 -0
- package/lib/importData.js +50 -0
- package/lib/mcp/setup.d.ts +0 -2
- package/lib/mcp/setup.js +0 -24
- package/lib/process.js +15 -4
- package/lib/projectProfiles.d.ts +1 -1
- package/lib/projectProfiles.js +10 -2
- package/lib/projects/__tests__/AppDevModeInterface.test.js +3 -3
- package/lib/projects/__tests__/LocalDevProcess.test.js +5 -95
- package/lib/projects/__tests__/LocalDevWebsocketServer.test.js +6 -6
- package/lib/projects/__tests__/components.test.js +164 -7
- package/lib/projects/__tests__/localDevProjectHelpers.test.d.ts +1 -0
- package/lib/projects/__tests__/localDevProjectHelpers.test.js +118 -0
- package/lib/projects/add/v3AddComponent.js +16 -4
- package/lib/projects/components.d.ts +1 -0
- package/lib/projects/components.js +27 -1
- package/lib/projects/localDev/AppDevModeInterface.js +35 -3
- package/lib/projects/localDev/LocalDevLogger.d.ts +0 -4
- package/lib/projects/localDev/LocalDevLogger.js +2 -19
- package/lib/projects/localDev/LocalDevManager.js +1 -1
- package/lib/projects/localDev/LocalDevProcess.d.ts +1 -2
- package/lib/projects/localDev/LocalDevProcess.js +3 -26
- package/lib/projects/localDev/LocalDevState.d.ts +6 -7
- package/lib/projects/localDev/LocalDevState.js +16 -15
- package/lib/projects/localDev/LocalDevWebsocketServer.d.ts +1 -0
- package/lib/projects/localDev/LocalDevWebsocketServer.js +17 -2
- package/lib/projects/localDev/{helpers.d.ts → helpers/account.d.ts} +1 -7
- package/lib/projects/localDev/{helpers.js → helpers/account.js} +44 -144
- package/lib/projects/localDev/helpers/project.d.ts +12 -0
- package/lib/projects/localDev/helpers/project.js +173 -0
- package/lib/projects/urls.d.ts +1 -0
- package/lib/projects/urls.js +4 -0
- package/lib/prompts/__tests__/createFunctionPrompt.test.d.ts +1 -0
- package/lib/prompts/__tests__/createFunctionPrompt.test.js +129 -0
- package/lib/prompts/__tests__/createModulePrompt.test.d.ts +1 -0
- package/lib/prompts/__tests__/createModulePrompt.test.js +187 -0
- package/lib/prompts/__tests__/createTemplatePrompt.test.d.ts +1 -0
- package/lib/prompts/__tests__/createTemplatePrompt.test.js +102 -0
- package/lib/prompts/confirmImportDataPrompt.d.ts +1 -0
- package/lib/prompts/confirmImportDataPrompt.js +12 -0
- package/lib/prompts/createFunctionPrompt.d.ts +2 -1
- package/lib/prompts/createFunctionPrompt.js +36 -7
- package/lib/prompts/createModulePrompt.d.ts +2 -1
- package/lib/prompts/createModulePrompt.js +48 -1
- package/lib/prompts/createTemplatePrompt.d.ts +3 -24
- package/lib/prompts/createTemplatePrompt.js +9 -1
- package/lib/prompts/importDataFilePathPrompt.d.ts +1 -0
- package/lib/prompts/importDataFilePathPrompt.js +24 -0
- package/lib/prompts/importDataTestAccountSelectPrompt.d.ts +3 -0
- package/lib/prompts/importDataTestAccountSelectPrompt.js +29 -0
- package/lib/prompts/projectDevTargetAccountPrompt.js +1 -0
- package/lib/prompts/promptUtils.d.ts +7 -1
- package/lib/prompts/promptUtils.js +14 -1
- package/lib/ui/__tests__/removeAnsiCodes.test.d.ts +1 -0
- package/lib/ui/__tests__/removeAnsiCodes.test.js +84 -0
- package/lib/ui/index.js +3 -6
- package/lib/ui/removeAnsiCodes.d.ts +1 -0
- package/lib/ui/removeAnsiCodes.js +4 -0
- package/mcp-server/server.js +2 -1
- package/mcp-server/tools/cms/HsListTool.d.ts +23 -0
- package/mcp-server/tools/cms/HsListTool.js +58 -0
- package/mcp-server/tools/cms/__tests__/HsListTool.test.d.ts +1 -0
- package/mcp-server/tools/cms/__tests__/HsListTool.test.js +120 -0
- package/mcp-server/tools/index.d.ts +1 -0
- package/mcp-server/tools/index.js +8 -0
- package/mcp-server/tools/project/DocFetchTool.d.ts +17 -0
- package/mcp-server/tools/project/DocFetchTool.js +49 -0
- package/mcp-server/tools/project/DocsSearchTool.d.ts +26 -0
- package/mcp-server/tools/project/DocsSearchTool.js +62 -0
- package/mcp-server/tools/project/GetConfigValuesTool.js +3 -2
- package/mcp-server/tools/project/__tests__/DocFetchTool.test.d.ts +1 -0
- package/mcp-server/tools/project/__tests__/DocFetchTool.test.js +117 -0
- package/mcp-server/tools/project/__tests__/DocsSearchTool.test.d.ts +1 -0
- package/mcp-server/tools/project/__tests__/DocsSearchTool.test.js +190 -0
- package/mcp-server/tools/project/__tests__/GetConfigValuesTool.test.js +1 -1
- package/mcp-server/tools/project/constants.d.ts +2 -0
- package/mcp-server/tools/project/constants.js +6 -0
- package/mcp-server/utils/toolUsageTracking.d.ts +3 -1
- package/mcp-server/utils/toolUsageTracking.js +2 -1
- package/package.json +9 -6
- package/types/Cms.d.ts +16 -0
- package/types/Cms.js +25 -1
- package/types/LocalDev.d.ts +0 -3
- package/types/Prompts.d.ts +1 -0
- package/types/Yargs.d.ts +1 -1
- package/ui/index.d.ts +1 -0
- package/ui/index.js +6 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { removeAnsiCodes } from '../removeAnsiCodes.js';
|
|
4
|
+
describe('removeAnsiCodes', () => {
|
|
5
|
+
describe('basic functionality', () => {
|
|
6
|
+
it('should remove ANSI codes from colored text', () => {
|
|
7
|
+
const coloredText = chalk.red('Error message');
|
|
8
|
+
const cleanText = removeAnsiCodes(coloredText);
|
|
9
|
+
expect(cleanText).toBe('Error message');
|
|
10
|
+
});
|
|
11
|
+
it('should return unchanged text when no ANSI codes are present', () => {
|
|
12
|
+
const plainText = 'This is plain text';
|
|
13
|
+
const result = removeAnsiCodes(plainText);
|
|
14
|
+
expect(result).toBe('This is plain text');
|
|
15
|
+
});
|
|
16
|
+
it('should handle empty strings', () => {
|
|
17
|
+
const result = removeAnsiCodes('');
|
|
18
|
+
expect(result).toBe('');
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
describe('background colors', () => {
|
|
22
|
+
it('should remove background color codes', () => {
|
|
23
|
+
const text = chalk.bgRed('Text with red background');
|
|
24
|
+
expect(removeAnsiCodes(text)).toBe('Text with red background');
|
|
25
|
+
});
|
|
26
|
+
it('should remove multiple background colors', () => {
|
|
27
|
+
const text = chalk.bgBlue('Blue bg') + ' ' + chalk.bgYellow('Yellow bg');
|
|
28
|
+
expect(removeAnsiCodes(text)).toBe('Blue bg Yellow bg');
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
describe('text formatting', () => {
|
|
32
|
+
it('should remove bold formatting', () => {
|
|
33
|
+
const text = chalk.bold('Bold text');
|
|
34
|
+
expect(removeAnsiCodes(text)).toBe('Bold text');
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
describe('combined styles', () => {
|
|
38
|
+
it('should remove multiple formatting styles', () => {
|
|
39
|
+
const text = chalk.bold.red('Bold red text');
|
|
40
|
+
expect(removeAnsiCodes(text)).toBe('Bold red text');
|
|
41
|
+
});
|
|
42
|
+
it('should remove complex combinations', () => {
|
|
43
|
+
const text = chalk.bold.underline.red.bgYellow('Complex styling');
|
|
44
|
+
expect(removeAnsiCodes(text)).toBe('Complex styling');
|
|
45
|
+
});
|
|
46
|
+
it('should handle chained styles', () => {
|
|
47
|
+
const text = chalk.red('Red') + chalk.green(' Green') + chalk.blue(' Blue');
|
|
48
|
+
expect(removeAnsiCodes(text)).toBe('Red Green Blue');
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
describe('multiline text', () => {
|
|
52
|
+
it('should remove ANSI codes from multiline text', () => {
|
|
53
|
+
const text = chalk.red('Line 1\n') + chalk.green('Line 2\n') + chalk.blue('Line 3');
|
|
54
|
+
const expected = 'Line 1\nLine 2\nLine 3';
|
|
55
|
+
expect(removeAnsiCodes(text)).toBe(expected);
|
|
56
|
+
});
|
|
57
|
+
it('should handle mixed formatted and plain lines', () => {
|
|
58
|
+
const text = chalk.bold('Formatted line\n') +
|
|
59
|
+
'Plain line\n' +
|
|
60
|
+
chalk.italic('Another formatted line');
|
|
61
|
+
const expected = 'Formatted line\nPlain line\nAnother formatted line';
|
|
62
|
+
expect(removeAnsiCodes(text)).toBe(expected);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
describe('edge cases', () => {
|
|
66
|
+
it('should handle text with only ANSI codes (no visible text)', () => {
|
|
67
|
+
const text = chalk.red('');
|
|
68
|
+
expect(removeAnsiCodes(text)).toBe('');
|
|
69
|
+
});
|
|
70
|
+
it('should handle multiple consecutive ANSI codes', () => {
|
|
71
|
+
// Create text with multiple style applications
|
|
72
|
+
const text = chalk.red(chalk.bold(chalk.underline('Heavily styled')));
|
|
73
|
+
expect(removeAnsiCodes(text)).toBe('Heavily styled');
|
|
74
|
+
});
|
|
75
|
+
it('should handle mixed content with spaces and special characters', () => {
|
|
76
|
+
const text = chalk.green('Success:') + ' ' + chalk.red('Error!') + ' @#$%^&*()';
|
|
77
|
+
expect(removeAnsiCodes(text)).toBe('Success: Error! @#$%^&*()');
|
|
78
|
+
});
|
|
79
|
+
it('should handle text with tabs and newlines', () => {
|
|
80
|
+
const text = chalk.yellow('\tTabbed text\n') + chalk.cyan('New line text');
|
|
81
|
+
expect(removeAnsiCodes(text)).toBe('\tTabbed text\nNew line text');
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
});
|
package/lib/ui/index.js
CHANGED
|
@@ -65,16 +65,13 @@ export function uiCommandReference(command, withQuotes = true) {
|
|
|
65
65
|
}
|
|
66
66
|
export function uiFeatureHighlight(features, title) {
|
|
67
67
|
uiInfoSection(title ? title : i18n(`lib.ui.featureHighlight.defaultTitle`), () => {
|
|
68
|
-
features.forEach(
|
|
69
|
-
const featureKey = `lib.ui.featureHighlight.featureKeys.${
|
|
68
|
+
features.forEach(feature => {
|
|
69
|
+
const featureKey = `lib.ui.featureHighlight.featureKeys.${feature}`;
|
|
70
70
|
const message = i18n(`${featureKey}.message`, {
|
|
71
71
|
command: uiCommandReference(i18n(`${featureKey}.command`)),
|
|
72
72
|
link: uiLink(i18n(`${featureKey}.linkText`), i18n(`${featureKey}.url`)),
|
|
73
73
|
});
|
|
74
|
-
|
|
75
|
-
logger.log('');
|
|
76
|
-
}
|
|
77
|
-
logger.log(message);
|
|
74
|
+
logger.log(` - ${message}`);
|
|
78
75
|
});
|
|
79
76
|
});
|
|
80
77
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function removeAnsiCodes(str: string): string;
|
package/mcp-server/server.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
-
import { registerProjectTools } from './tools/index.js';
|
|
3
|
+
import { registerProjectTools, registerCmsTools } from './tools/index.js';
|
|
4
4
|
const server = new McpServer({
|
|
5
5
|
name: 'HubSpot CLI MCP Server',
|
|
6
6
|
version: '0.0.1',
|
|
@@ -11,6 +11,7 @@ const server = new McpServer({
|
|
|
11
11
|
},
|
|
12
12
|
});
|
|
13
13
|
registerProjectTools(server);
|
|
14
|
+
registerCmsTools(server);
|
|
14
15
|
// Start receiving messages on stdin and sending messages on stdout
|
|
15
16
|
const transport = new StdioServerTransport();
|
|
16
17
|
server.connect(transport);
|
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
path: z.ZodOptional<z.ZodString>;
|
|
7
|
+
account: z.ZodOptional<z.ZodString>;
|
|
8
|
+
}, "strip", z.ZodTypeAny, {
|
|
9
|
+
absoluteCurrentWorkingDirectory: string;
|
|
10
|
+
account?: string | undefined;
|
|
11
|
+
path?: string | undefined;
|
|
12
|
+
}, {
|
|
13
|
+
absoluteCurrentWorkingDirectory: string;
|
|
14
|
+
account?: string | undefined;
|
|
15
|
+
path?: string | undefined;
|
|
16
|
+
}>;
|
|
17
|
+
export type HsListInputSchema = z.infer<typeof inputSchemaZodObject>;
|
|
18
|
+
export declare class HsListTool extends Tool<HsListInputSchema> {
|
|
19
|
+
constructor(mcpServer: McpServer);
|
|
20
|
+
handler({ path, account, absoluteCurrentWorkingDirectory, }: HsListInputSchema): Promise<TextContentResponse>;
|
|
21
|
+
register(): RegisteredTool;
|
|
22
|
+
}
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Tool } from '../../types.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { addFlag } from '../../utils/command.js';
|
|
4
|
+
import { absoluteCurrentWorkingDirectory } from '../project/constants.js';
|
|
5
|
+
import { runCommandInDir } from '../../utils/project.js';
|
|
6
|
+
import { formatTextContents } from '../../utils/content.js';
|
|
7
|
+
import { trackToolUsage } from '../../utils/toolUsageTracking.js';
|
|
8
|
+
const inputSchema = {
|
|
9
|
+
absoluteCurrentWorkingDirectory,
|
|
10
|
+
path: z
|
|
11
|
+
.string()
|
|
12
|
+
.describe('The remote directory path in the HubSpot CMS to list contents. If not specified, lists the root directory.')
|
|
13
|
+
.optional(),
|
|
14
|
+
account: z
|
|
15
|
+
.string()
|
|
16
|
+
.describe('The HubSpot account id or name from the HubSpot config file to use for the operation.')
|
|
17
|
+
.optional(),
|
|
18
|
+
};
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
20
|
+
const inputSchemaZodObject = z.object({ ...inputSchema });
|
|
21
|
+
const toolName = 'list-hubspot-cms-remote-contents';
|
|
22
|
+
export class HsListTool extends Tool {
|
|
23
|
+
constructor(mcpServer) {
|
|
24
|
+
super(mcpServer);
|
|
25
|
+
}
|
|
26
|
+
async handler({ path, account, absoluteCurrentWorkingDirectory, }) {
|
|
27
|
+
await trackToolUsage(toolName);
|
|
28
|
+
let command = 'hs list';
|
|
29
|
+
if (path) {
|
|
30
|
+
command += ` ${path}`;
|
|
31
|
+
}
|
|
32
|
+
if (account) {
|
|
33
|
+
command = addFlag(command, 'account', account);
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const { stdout, stderr } = await runCommandInDir(absoluteCurrentWorkingDirectory, command);
|
|
37
|
+
return formatTextContents(stdout, stderr);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
41
|
+
return {
|
|
42
|
+
content: [
|
|
43
|
+
{
|
|
44
|
+
type: 'text',
|
|
45
|
+
text: `Error executing hs list command: ${errorMessage}`,
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
register() {
|
|
52
|
+
return this.mcpServer.registerTool(toolName, {
|
|
53
|
+
title: 'List HubSpot CMS Directory Contents',
|
|
54
|
+
description: 'List remote contents of a HubSpot CMS directory.',
|
|
55
|
+
inputSchema,
|
|
56
|
+
}, this.handler);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -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
|
+
});
|
|
@@ -5,6 +5,9 @@ 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';
|
|
8
11
|
export function registerProjectTools(mcpServer) {
|
|
9
12
|
return [
|
|
10
13
|
new UploadProjectTools(mcpServer).register(),
|
|
@@ -14,5 +17,10 @@ export function registerProjectTools(mcpServer) {
|
|
|
14
17
|
new AddFeatureToProjectTool(mcpServer).register(),
|
|
15
18
|
new ValidateProjectTool(mcpServer).register(),
|
|
16
19
|
new GetConfigValuesTool(mcpServer).register(),
|
|
20
|
+
new DocsSearchTool(mcpServer).register(),
|
|
21
|
+
new DocFetchTool(mcpServer).register(),
|
|
17
22
|
];
|
|
18
23
|
}
|
|
24
|
+
export function registerCmsTools(mcpServer) {
|
|
25
|
+
return [new HsListTool(mcpServer).register()];
|
|
26
|
+
}
|
|
@@ -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-
|
|
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: '',
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|