@splunk/splunk-ui-mcp 0.1.0 → 0.3.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 (60) hide show
  1. package/README.md +19 -5
  2. package/lib/constants/versions.js +3 -3
  3. package/lib/index.js +16 -4
  4. package/lib/prompts/audit/audit-prompt.d.ts +2 -0
  5. package/lib/prompts/audit/audit-prompt.js +11 -0
  6. package/lib/prompts/audit/audit.d.ts +16 -0
  7. package/lib/prompts/audit/audit.js +36 -0
  8. package/lib/prompts/audit/index.d.ts +1 -0
  9. package/lib/prompts/audit/index.js +1 -0
  10. package/lib/prompts/build-ui/build-figma-design-prompt.d.ts +2 -0
  11. package/lib/prompts/build-ui/build-figma-design-prompt.js +9 -0
  12. package/lib/prompts/build-ui/build-ui-prompt.d.ts +2 -0
  13. package/lib/prompts/build-ui/build-ui-prompt.js +8 -0
  14. package/lib/prompts/build-ui/build-ui.d.ts +16 -0
  15. package/lib/prompts/build-ui/build-ui.js +62 -0
  16. package/lib/prompts/build-ui/index.d.ts +1 -0
  17. package/lib/prompts/build-ui/index.js +1 -0
  18. package/lib/prompts/create-scaffold/create-scaffold-prompt.d.ts +2 -0
  19. package/lib/prompts/create-scaffold/create-scaffold-prompt.js +34 -0
  20. package/lib/prompts/create-scaffold/create-scaffold.d.ts +16 -0
  21. package/lib/prompts/create-scaffold/create-scaffold.js +60 -0
  22. package/lib/prompts/create-scaffold/index.d.ts +1 -0
  23. package/lib/prompts/create-scaffold/index.js +1 -0
  24. package/lib/prompts/session-feedback/index.d.ts +1 -0
  25. package/lib/prompts/session-feedback/index.js +1 -0
  26. package/lib/prompts/session-feedback/session-feedback-prompt.d.ts +2 -0
  27. package/lib/prompts/session-feedback/session-feedback-prompt.js +113 -0
  28. package/lib/prompts/session-feedback/session-feedback.d.ts +11 -0
  29. package/lib/prompts/session-feedback/session-feedback.js +22 -0
  30. package/lib/prompts/types.d.ts +8 -0
  31. package/lib/resources/components.js +12 -6
  32. package/lib/resources/design-tokens.d.ts +17 -0
  33. package/lib/resources/design-tokens.js +115 -0
  34. package/lib/tools/find_component.d.ts +19 -0
  35. package/lib/tools/find_component.js +93 -0
  36. package/lib/tools/find_design_token.d.ts +16 -0
  37. package/lib/tools/find_design_token.js +134 -0
  38. package/lib/tools/{find_icons.d.ts → find_icon.d.ts} +2 -2
  39. package/lib/tools/{find_icons.js → find_icon.js} +5 -5
  40. package/lib/utils/component-catalog.d.ts +19 -2
  41. package/lib/utils/component-catalog.js +77 -29
  42. package/lib/utils/design-token-catalog.d.ts +37 -0
  43. package/lib/utils/design-token-catalog.js +166 -0
  44. package/lib/utils/package-assets.d.ts +8 -0
  45. package/lib/utils/package-assets.js +35 -0
  46. package/package.json +16 -10
  47. package/lib/resources/tests/components.unit.js +0 -133
  48. package/lib/resources/tests/icons.unit.d.ts +0 -1
  49. package/lib/resources/tests/icons.unit.js +0 -161
  50. package/lib/tools/get_component_docs.d.ts +0 -19
  51. package/lib/tools/get_component_docs.js +0 -82
  52. package/lib/tools/tests/find_icons.unit.d.ts +0 -1
  53. package/lib/tools/tests/find_icons.unit.js +0 -149
  54. package/lib/tools/tests/get_component_docs.unit.d.ts +0 -1
  55. package/lib/tools/tests/get_component_docs.unit.js +0 -131
  56. package/lib/tools/tests/requirements.unit.d.ts +0 -1
  57. package/lib/tools/tests/requirements.unit.js +0 -34
  58. package/lib/utils/tests/component-catalog.unit.d.ts +0 -1
  59. package/lib/utils/tests/component-catalog.unit.js +0 -144
  60. /package/lib/{resources/tests/components.unit.d.ts → prompts/types.js} +0 -0
@@ -0,0 +1,17 @@
1
+ import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { ResourceLink, ReadResourceResult } from '@modelcontextprotocol/sdk/types.js';
3
+ export declare const DESIGN_TOKEN_RESOURCE_NAME = "splunk-ui-design-tokens";
4
+ export declare const DESIGN_TOKEN_RESOURCE_URI_BASE = "mcp://splunk-ui/design-tokens";
5
+ export declare const createDesignTokenResourceUri: (tokenName: string) => string;
6
+ export declare const createDesignTokenResourceLink: (tokenName: string) => ResourceLink;
7
+ declare const designTokenResource: {
8
+ name: string;
9
+ template: ResourceTemplate;
10
+ config: {
11
+ title: string;
12
+ description: string;
13
+ mimeType: string;
14
+ };
15
+ handler: (uri: URL, params: Record<string, unknown>) => ReadResourceResult;
16
+ };
17
+ export default designTokenResource;
@@ -0,0 +1,115 @@
1
+ import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { filterByKeywords, stringToKeywords } from '@splunk/ui-utils/filter.js';
3
+ import { extractReplacementTokenName, getDesignTokenInfo, getDesignTokenList, } from "../utils/design-token-catalog.js";
4
+ export const DESIGN_TOKEN_RESOURCE_NAME = 'splunk-ui-design-tokens';
5
+ export const DESIGN_TOKEN_RESOURCE_URI_BASE = 'mcp://splunk-ui/design-tokens';
6
+ export const createDesignTokenResourceUri = (tokenName) => `${DESIGN_TOKEN_RESOURCE_URI_BASE}/${encodeURIComponent(tokenName)}`;
7
+ const createDesignTokenResourceMetadata = (tokenName) => {
8
+ return {
9
+ name: tokenName,
10
+ title: tokenName,
11
+ uri: createDesignTokenResourceUri(tokenName),
12
+ description: getDesignTokenInfo(tokenName)?.description,
13
+ mimeType: 'application/json',
14
+ };
15
+ };
16
+ export const createDesignTokenResourceLink = (tokenName) => ({
17
+ type: 'resource_link',
18
+ ...createDesignTokenResourceMetadata(tokenName),
19
+ });
20
+ const createDesignTokenResourcePayload = (token) => {
21
+ const replacementToken = extractReplacementTokenName(token.deprecated);
22
+ return {
23
+ name: token.name,
24
+ description: token.description,
25
+ type: token.type,
26
+ deprecated: token.deprecated,
27
+ usage: `variables.${token.name}`,
28
+ replacementToken,
29
+ replacementUsage: replacementToken ? `variables.${replacementToken}` : undefined,
30
+ cssProperties: token.cssProperties,
31
+ figmaVariableScopes: token.figmaVariableScopes,
32
+ };
33
+ };
34
+ const createListResult = () => ({
35
+ resources: getDesignTokenList().map((token) => ({
36
+ name: token.name,
37
+ title: token.name,
38
+ uri: createDesignTokenResourceUri(token.name),
39
+ description: token.description,
40
+ mimeType: 'application/json',
41
+ })),
42
+ });
43
+ const readDesignTokenResource = (tokenName) => {
44
+ const token = getDesignTokenInfo(tokenName);
45
+ if (!token) {
46
+ throw new Error(`Unknown design token "${tokenName}"`);
47
+ }
48
+ const resourcePayload = createDesignTokenResourcePayload(token);
49
+ return {
50
+ contents: [
51
+ {
52
+ uri: createDesignTokenResourceUri(token.name),
53
+ mimeType: 'application/json',
54
+ text: `${JSON.stringify(resourcePayload, null, 2)}\n`,
55
+ },
56
+ ],
57
+ };
58
+ };
59
+ const designTokenResourceTemplate = new ResourceTemplate(`${DESIGN_TOKEN_RESOURCE_URI_BASE}/{tokenName}`, {
60
+ list: async () => createListResult(),
61
+ complete: {
62
+ tokenName: async (value) => {
63
+ const input = value?.toString() ?? '';
64
+ const tokenNames = getDesignTokenList().map((token) => token.name);
65
+ if (!input) {
66
+ return tokenNames.slice(0, 50);
67
+ }
68
+ if (stringToKeywords(input).filter(Boolean).length === 0) {
69
+ return [];
70
+ }
71
+ return filterByKeywords(tokenNames, input).slice(0, 50);
72
+ },
73
+ },
74
+ });
75
+ const designTokenResource = {
76
+ name: DESIGN_TOKEN_RESOURCE_NAME,
77
+ template: designTokenResourceTemplate,
78
+ config: {
79
+ title: 'Splunk UI Design Tokens',
80
+ description: 'Metadata and usage guidance for design tokens from @splunk/themes.',
81
+ mimeType: 'application/json',
82
+ },
83
+ handler: (uri, params) => {
84
+ const { tokenName: tokenNameParam } = params;
85
+ let tokenName;
86
+ if (typeof tokenNameParam === 'string' && tokenNameParam.length > 0) {
87
+ try {
88
+ tokenName = decodeURIComponent(tokenNameParam);
89
+ }
90
+ catch {
91
+ throw new Error(`Invalid design token name in resource params: ${tokenNameParam}`);
92
+ }
93
+ }
94
+ else {
95
+ const pathParts = uri.pathname.split('/').filter(Boolean);
96
+ const encodedTokenName = pathParts[pathParts.length - 1];
97
+ if (encodedTokenName === 'design-tokens') {
98
+ tokenName = undefined;
99
+ }
100
+ else if (encodedTokenName) {
101
+ try {
102
+ tokenName = decodeURIComponent(encodedTokenName);
103
+ }
104
+ catch {
105
+ throw new Error(`Invalid design token name in resource path: ${encodedTokenName}`);
106
+ }
107
+ }
108
+ }
109
+ if (!tokenName) {
110
+ throw new Error(`Design token name missing in resource request: ${uri.toString()}`);
111
+ }
112
+ return readDesignTokenResource(tokenName);
113
+ },
114
+ };
115
+ export default designTokenResource;
@@ -0,0 +1,19 @@
1
+ import { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
2
+ import { z } from 'zod';
3
+ export declare const findComponentHandler: ({ query }: {
4
+ query?: string;
5
+ }) => CallToolResult;
6
+ declare const findComponentTool: {
7
+ name: string;
8
+ config: {
9
+ title: string;
10
+ description: string;
11
+ inputSchema: {
12
+ query: z.ZodString;
13
+ };
14
+ };
15
+ handler: ({ query }: {
16
+ query?: string;
17
+ }) => CallToolResult;
18
+ };
19
+ export default findComponentTool;
@@ -0,0 +1,93 @@
1
+ import { z } from 'zod';
2
+ import { createComponentResourceLink } from "../resources/components.js";
3
+ import { getComponentDocs, getComponentInfo, getComponentNames, searchComponents, } from "../utils/component-catalog.js";
4
+ const MAX_SUGGESTIONS = 5;
5
+ const MAX_EXAMPLES = 10;
6
+ const CATEGORY_SEARCH_MIN_QUERY_LENGTH = 3;
7
+ const getSuggestedComponents = (trimmedQuery) => {
8
+ const includeCategory = trimmedQuery.length >= CATEGORY_SEARCH_MIN_QUERY_LENGTH;
9
+ return searchComponents(trimmedQuery, {
10
+ includeCategory,
11
+ limit: MAX_SUGGESTIONS,
12
+ });
13
+ };
14
+ export const findComponentHandler = ({ query }) => {
15
+ if (!query || query.trim().length === 0) {
16
+ return {
17
+ content: [
18
+ {
19
+ type: 'text',
20
+ text: 'Component query is required. Provide a component name or keywords.',
21
+ },
22
+ ],
23
+ isError: true,
24
+ };
25
+ }
26
+ const normalizedQuery = query.trim();
27
+ const componentInfo = getComponentInfo(normalizedQuery);
28
+ if (componentInfo) {
29
+ try {
30
+ const docs = getComponentDocs(componentInfo.name);
31
+ return {
32
+ content: [
33
+ {
34
+ type: 'text',
35
+ text: docs,
36
+ },
37
+ createComponentResourceLink(componentInfo.name),
38
+ ],
39
+ structuredContent: {
40
+ query: normalizedQuery,
41
+ resultType: 'exact',
42
+ component: componentInfo.name,
43
+ },
44
+ };
45
+ }
46
+ catch (error) {
47
+ return {
48
+ content: [
49
+ {
50
+ type: 'text',
51
+ text: `Failed to retrieve documentation for "${componentInfo.name}": ${error.message}`,
52
+ },
53
+ ],
54
+ isError: true,
55
+ };
56
+ }
57
+ }
58
+ const suggestions = getSuggestedComponents(normalizedQuery);
59
+ const sampleNames = getComponentNames().slice(0, MAX_EXAMPLES).join(', ');
60
+ const hasSuggestions = suggestions.length > 0;
61
+ const suggestionText = hasSuggestions
62
+ ? `Top matches: ${suggestions.join(', ')}.\n\nUse one of the resource links below to open docs directly.`
63
+ : `No close matches found.\n\nAvailable components include: ${sampleNames}, and more.`;
64
+ return {
65
+ content: [
66
+ {
67
+ type: 'text',
68
+ text: `No exact component match for "${normalizedQuery}".\n\n${suggestionText}`,
69
+ },
70
+ ...suggestions.map((name) => createComponentResourceLink(name)),
71
+ ],
72
+ structuredContent: {
73
+ query: normalizedQuery,
74
+ resultType: 'suggestions',
75
+ suggestions,
76
+ },
77
+ };
78
+ };
79
+ const findComponentTool = {
80
+ name: 'find_component',
81
+ config: {
82
+ title: 'Find Component Documentation',
83
+ description: 'Finds @splunk/react-ui components by exact name or fuzzy query. ' +
84
+ 'Returns full docs for exact matches and ranked suggestions for discovery queries.',
85
+ inputSchema: {
86
+ query: z
87
+ .string()
88
+ .describe('Component name or keywords (e.g., "Button", "CardLayout", "table", "form rows").'),
89
+ },
90
+ },
91
+ handler: findComponentHandler,
92
+ };
93
+ export default findComponentTool;
@@ -0,0 +1,16 @@
1
+ import { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
2
+ import { z } from 'zod';
3
+ declare const findDesignTokenTool: {
4
+ name: string;
5
+ config: {
6
+ title: string;
7
+ description: string;
8
+ inputSchema: {
9
+ query: z.ZodString;
10
+ };
11
+ };
12
+ handler: ({ query }: {
13
+ query?: string;
14
+ }) => CallToolResult;
15
+ };
16
+ export default findDesignTokenTool;
@@ -0,0 +1,134 @@
1
+ import { z } from 'zod';
2
+ import { createDesignTokenResourceLink, createDesignTokenResourceUri, } from "../resources/design-tokens.js";
3
+ import { extractReplacementTokenName, findDesignTokens, getDesignTokenInfo, isDeprecatedToken, } from "../utils/design-token-catalog.js";
4
+ const NO_RESULTS_RESULT = {
5
+ content: [
6
+ {
7
+ type: 'text',
8
+ text: 'No design tokens found. Try keywords like "text", "background", "border", ' +
9
+ '"spacing", or "shadow".',
10
+ },
11
+ ],
12
+ };
13
+ const getReplacementToken = (token) => {
14
+ if (!token || !isDeprecatedToken(token)) {
15
+ return undefined;
16
+ }
17
+ const replacementTokenName = extractReplacementTokenName(token.deprecated);
18
+ return replacementTokenName ? getDesignTokenInfo(replacementTokenName) : undefined;
19
+ };
20
+ const addUsageReference = (token) => ({
21
+ name: token.name,
22
+ description: token.description,
23
+ type: token.type,
24
+ deprecated: token.deprecated,
25
+ usage: `variables.${token.name}`,
26
+ cssProperties: token.cssProperties,
27
+ figmaVariableScopes: token.figmaVariableScopes,
28
+ });
29
+ const formatTokenDetails = (token, options = {}) => {
30
+ const lines = [];
31
+ if (token.description) {
32
+ lines.push(`- Description: ${token.description}`);
33
+ }
34
+ if (token.type) {
35
+ lines.push(`- Type: \`${token.type}\``);
36
+ }
37
+ lines.push(`- Usage: \`variables.${token.name}\``);
38
+ if (token.cssProperties?.length) {
39
+ lines.push(`- CSS properties: ${token.cssProperties.map((p) => `\`${p}\``).join(', ')}`);
40
+ }
41
+ if (token.figmaVariableScopes?.length) {
42
+ lines.push(`- Figma variable scopes: ${token.figmaVariableScopes.map((p) => `\`${p}\``).join(', ')}`);
43
+ }
44
+ if (isDeprecatedToken(token)) {
45
+ lines.push(options.deprecatedAsBoolean
46
+ ? '- Deprecated: true'
47
+ : `- Deprecated: ${String(token.deprecated)}`);
48
+ }
49
+ const resourceLabel = options.resourceLabel ?? 'Resource';
50
+ lines.push(`- ${resourceLabel}: \`${createDesignTokenResourceUri(token.name)}\``);
51
+ return lines.join('\n');
52
+ };
53
+ const findDesignTokenHandler = ({ query }) => {
54
+ const normalizedQuery = query?.trim() ?? '';
55
+ if (normalizedQuery.length === 0) {
56
+ return NO_RESULTS_RESULT;
57
+ }
58
+ const exactMatch = getDesignTokenInfo(normalizedQuery);
59
+ const deprecatedExactMatch = exactMatch && isDeprecatedToken(exactMatch) ? exactMatch : undefined;
60
+ const exactReplacement = deprecatedExactMatch
61
+ ? getReplacementToken(deprecatedExactMatch)
62
+ : undefined;
63
+ const searchResults = exactMatch ? [] : findDesignTokens(normalizedQuery);
64
+ if (!exactMatch && searchResults.length === 0) {
65
+ return NO_RESULTS_RESULT;
66
+ }
67
+ let results = searchResults;
68
+ if (exactReplacement) {
69
+ results = [exactReplacement];
70
+ }
71
+ else if (exactMatch) {
72
+ results = [exactMatch];
73
+ }
74
+ const [recommended, ...others] = results;
75
+ const alternativeTokens = others.slice(0, 4);
76
+ const summarySections = [];
77
+ if (exactMatch && !deprecatedExactMatch) {
78
+ summarySections.push(`# \`${exactMatch.name}\``, formatTokenDetails(recommended, { resourceLabel: 'Resource link' }), 'Follow the resource links below for full token metadata.');
79
+ }
80
+ else if (deprecatedExactMatch && !exactReplacement) {
81
+ const deprecatedGuidance = typeof deprecatedExactMatch.deprecated === 'string'
82
+ ? `Query matched deprecated token \`${deprecatedExactMatch.name}\`. ${deprecatedExactMatch.deprecated}`
83
+ : `Query matched deprecated token \`${deprecatedExactMatch.name}\`. No replacement recommendation is available from token metadata.`;
84
+ summarySections.push(`# \`${deprecatedExactMatch.name}\``, deprecatedGuidance, formatTokenDetails(recommended, { deprecatedAsBoolean: true }), 'Follow the resource links below for full token metadata.');
85
+ }
86
+ else if (deprecatedExactMatch && exactReplacement) {
87
+ const deprecatedGuidance = typeof deprecatedExactMatch.deprecated === 'string'
88
+ ? `Query matched deprecated token \`${deprecatedExactMatch.name}\`. ${deprecatedExactMatch.deprecated}`
89
+ : `Query matched deprecated token \`${deprecatedExactMatch.name}\`.`;
90
+ summarySections.push(`# \`${deprecatedExactMatch.name}\``, deprecatedGuidance, '## Recommended Token', `Recommended to use \`${recommended.name}\` for "${normalizedQuery}"`, formatTokenDetails(recommended), 'Follow the resource links below for full token metadata.');
91
+ }
92
+ else {
93
+ summarySections.push(`# Design Tokens for "${normalizedQuery}"`, '## Recommended Token', `Recommended to use \`${recommended.name}\` for "${normalizedQuery}"`, formatTokenDetails(recommended), 'Follow the resource links below for full token metadata.');
94
+ }
95
+ if (alternativeTokens.length > 0) {
96
+ summarySections.push('## Alternative Tokens', alternativeTokens
97
+ .map((token) => `- \`${token.name}\`${token.description ? `: ${token.description}` : ''}`)
98
+ .join('\n'));
99
+ }
100
+ return {
101
+ content: [
102
+ {
103
+ type: 'text',
104
+ text: summarySections.join('\n\n'),
105
+ },
106
+ createDesignTokenResourceLink(recommended.name),
107
+ ...alternativeTokens.map((token) => createDesignTokenResourceLink(token.name)),
108
+ ],
109
+ structuredContent: {
110
+ recommended: addUsageReference(getDesignTokenInfo(recommended.name) ?? recommended),
111
+ alternatives: alternativeTokens
112
+ .map((token) => getDesignTokenInfo(token.name) ?? token)
113
+ .filter((token) => !isDeprecatedToken(token))
114
+ .map((token) => addUsageReference(token)),
115
+ },
116
+ };
117
+ };
118
+ const findDesignTokenTool = {
119
+ name: 'find_design_token',
120
+ config: {
121
+ title: 'Find Design Tokens',
122
+ description: 'Search for a Splunk UI design token from @splunk/themes. ' +
123
+ 'In Splunk UI projects, all styling must use styled-components and design tokens for CSS properties as much as possible — ' +
124
+ 'never hardcode hex values, rgb(), or CSS custom properties like var(--something). ' +
125
+ 'Call this tool whenever you need a color, spacing, typography, shadow, or z-index value.',
126
+ inputSchema: {
127
+ query: z
128
+ .string()
129
+ .describe('What you are trying to style, e.g. "button background", "paragraph text color", "input border", "modal background", "spacing between elements", "focus shadow".'),
130
+ },
131
+ },
132
+ handler: findDesignTokenHandler,
133
+ };
134
+ export default findDesignTokenTool;
@@ -1,6 +1,6 @@
1
1
  import { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
2
2
  import { z } from 'zod';
3
- declare const findIconsTool: {
3
+ declare const findIconTool: {
4
4
  name: string;
5
5
  config: {
6
6
  title: string;
@@ -13,4 +13,4 @@ declare const findIconsTool: {
13
13
  query?: string;
14
14
  }) => CallToolResult;
15
15
  };
16
- export default findIconsTool;
16
+ export default findIconTool;
@@ -30,7 +30,7 @@ const formatIconDetails = (details) => {
30
30
  lines.push(details.usageExample);
31
31
  return lines.join('\n');
32
32
  };
33
- const findIconsHandler = ({ query }) => {
33
+ const findIconHandler = ({ query }) => {
34
34
  if (!query || query.trim().length === 0) {
35
35
  return NO_RESULTS_RESULT;
36
36
  }
@@ -86,8 +86,8 @@ const findIconsHandler = ({ query }) => {
86
86
  structuredContent,
87
87
  };
88
88
  };
89
- const findIconsTool = {
90
- name: 'find_icons',
89
+ const findIconTool = {
90
+ name: 'find_icon',
91
91
  config: {
92
92
  title: 'Find Icons',
93
93
  description: 'Search @splunk/react-icons for the best match and a few alternatives based on your keyword.',
@@ -97,6 +97,6 @@ const findIconsTool = {
97
97
  .describe('The search query for the icon, e.g., "search", "notification", "user".'),
98
98
  },
99
99
  },
100
- handler: findIconsHandler,
100
+ handler: findIconHandler,
101
101
  };
102
- export default findIconsTool;
102
+ export default findIconTool;
@@ -8,14 +8,30 @@ export type ComponentInfo = {
8
8
  * @internal
9
9
  */
10
10
  export declare function setDocsLlmPath(path: string | null): void;
11
+ /**
12
+ * Normalizes component names for case-insensitive and format-insensitive matching.
13
+ * Examples:
14
+ * - "Card Layout" -> "cardlayout"
15
+ * - "CardLayout" -> "cardlayout"
16
+ * - "card-layout" -> "cardlayout"
17
+ */
18
+ export declare function normalizeComponentName(name: string): string;
11
19
  /**
12
20
  * Returns the cached list of all components
13
21
  */
14
22
  export declare function getComponentList(): ComponentInfo[];
15
23
  /**
16
- * Checks if a component exists in the catalog
24
+ * Returns the cached list of component names
25
+ */
26
+ export declare function getComponentNames(): string[];
27
+ type SearchComponentsOptions = {
28
+ includeCategory?: boolean;
29
+ limit?: number;
30
+ };
31
+ /**
32
+ * Finds components by keyword and normalized substring matching.
17
33
  */
18
- export declare function componentExists(componentName: string): boolean;
34
+ export declare function searchComponents(query: string, { limit, includeCategory }?: SearchComponentsOptions): string[];
19
35
  /**
20
36
  * Gets component info by name
21
37
  */
@@ -24,3 +40,4 @@ export declare function getComponentInfo(componentName: string): ComponentInfo |
24
40
  * Reads the markdown documentation for a component
25
41
  */
26
42
  export declare function getComponentDocs(componentName: string): string;
43
+ export {};
@@ -1,7 +1,11 @@
1
1
  import { readFileSync } from 'fs';
2
- import { join, dirname, resolve, relative, isAbsolute } from 'path';
3
- import { createRequire } from 'module';
2
+ import { join, resolve, relative, isAbsolute } from 'path';
3
+ import { filterByKeywords } from '@splunk/ui-utils/filter.js';
4
+ import { resolvePackageAssetPath } from "./package-assets.js";
4
5
  let componentCache = null;
6
+ let componentNameCache = null;
7
+ let componentInfoByNormalizedNameCache = null;
8
+ let componentSearchIndexCache = null;
5
9
  let docsLlmPath = null;
6
10
  /**
7
11
  * Override the docs-llm path (primarily for testing)
@@ -10,15 +14,31 @@ let docsLlmPath = null;
10
14
  export function setDocsLlmPath(path) {
11
15
  docsLlmPath = path;
12
16
  componentCache = null; // Clear cache when path changes
17
+ componentNameCache = null;
18
+ componentInfoByNormalizedNameCache = null;
19
+ componentSearchIndexCache = null;
13
20
  }
14
21
  /**
15
22
  * Sanitizes component names; Rejects names containing path separators or parent directory references
16
23
  */
17
24
  function sanitizeComponentName(name) {
18
- if (name.includes('/') || name.includes('\\') || name.includes('..')) {
25
+ const sanitizedName = name.trim();
26
+ if (sanitizedName.includes('/') ||
27
+ sanitizedName.includes('\\') ||
28
+ sanitizedName.includes('..')) {
19
29
  throw new Error(`Invalid component name: "${name}".`);
20
30
  }
21
- return name;
31
+ return sanitizedName;
32
+ }
33
+ /**
34
+ * Normalizes component names for case-insensitive and format-insensitive matching.
35
+ * Examples:
36
+ * - "Card Layout" -> "cardlayout"
37
+ * - "CardLayout" -> "cardlayout"
38
+ * - "card-layout" -> "cardlayout"
39
+ */
40
+ export function normalizeComponentName(name) {
41
+ return name.toLowerCase().replace(/[^a-z0-9]/g, '');
22
42
  }
23
43
  /**
24
44
  * Resolves the path to the @splunk/react-ui/docs-llm directory
@@ -31,23 +51,8 @@ function resolveDocsLlmPath() {
31
51
  if (docsLlmPath) {
32
52
  return docsLlmPath;
33
53
  }
34
- // Try resolving from the user's project first
35
- try {
36
- const require = createRequire(join(process.cwd(), 'package.json'));
37
- const reactUiPackagePath = require.resolve('@splunk/react-ui/package.json');
38
- const reactUiPath = dirname(reactUiPackagePath);
39
- docsLlmPath = join(reactUiPath, 'docs-llm');
40
- return docsLlmPath;
41
- }
42
- catch {
43
- // Project doesn't have @splunk/react-ui installed, fall back to MCP's bundled version
44
- }
45
- // Fall back to MCP's bundled version (for standalone usage via setup tool)
46
54
  try {
47
- const require = createRequire(import.meta.url);
48
- const reactUiPackagePath = require.resolve('@splunk/react-ui/package.json');
49
- const reactUiPath = dirname(reactUiPackagePath);
50
- docsLlmPath = join(reactUiPath, 'docs-llm');
55
+ docsLlmPath = resolvePackageAssetPath('@splunk/react-ui', 'docs-llm');
51
56
  return docsLlmPath;
52
57
  }
53
58
  catch (error) {
@@ -93,24 +98,67 @@ function parseComponentCatalog() {
93
98
  * Returns the cached list of all components
94
99
  */
95
100
  export function getComponentList() {
96
- if (!componentCache) {
97
- componentCache = parseComponentCatalog();
98
- }
101
+ componentCache ??= parseComponentCatalog();
99
102
  return componentCache;
100
103
  }
101
104
  /**
102
- * Checks if a component exists in the catalog
105
+ * Returns the cached list of component names
103
106
  */
104
- export function componentExists(componentName) {
105
- const components = getComponentList();
106
- return components.some((c) => c.name === componentName);
107
+ export function getComponentNames() {
108
+ componentNameCache ??= getComponentList().map((component) => component.name);
109
+ return componentNameCache;
110
+ }
111
+ function getComponentInfoByNormalizedNameMap() {
112
+ componentInfoByNormalizedNameCache ??= new Map(getComponentList().map((component) => [normalizeComponentName(component.name), component]));
113
+ return componentInfoByNormalizedNameCache;
114
+ }
115
+ function getComponentSearchIndex() {
116
+ componentSearchIndexCache ??= getComponentList().map((component) => {
117
+ const nameSearchText = component.name;
118
+ const nameAndCategorySearchText = component.category
119
+ ? `${component.name} ${component.category}`
120
+ : component.name;
121
+ return {
122
+ component,
123
+ nameSearchText,
124
+ nameAndCategorySearchText,
125
+ normalizedNameSearchText: normalizeComponentName(nameSearchText),
126
+ normalizedNameAndCategorySearchText: normalizeComponentName(nameAndCategorySearchText),
127
+ };
128
+ });
129
+ return componentSearchIndexCache;
130
+ }
131
+ /**
132
+ * Finds components by keyword and normalized substring matching.
133
+ */
134
+ export function searchComponents(query, { limit, includeCategory } = {}) {
135
+ const input = query.trim();
136
+ const normalizedInput = normalizeComponentName(input);
137
+ if (!normalizedInput) {
138
+ return [];
139
+ }
140
+ const searchIndex = getComponentSearchIndex();
141
+ const keywordMatches = filterByKeywords(searchIndex, input, (entry) => includeCategory ? entry.nameAndCategorySearchText : entry.nameSearchText).map((entry) => entry.component.name);
142
+ const normalizedMatches = searchIndex
143
+ .filter((entry) => (includeCategory
144
+ ? entry.normalizedNameAndCategorySearchText
145
+ : entry.normalizedNameSearchText).includes(normalizedInput))
146
+ .map((entry) => entry.component.name);
147
+ const uniqueMatches = [...new Set([...normalizedMatches, ...keywordMatches])];
148
+ if (limit && limit > 0) {
149
+ return uniqueMatches.slice(0, limit);
150
+ }
151
+ return uniqueMatches;
107
152
  }
108
153
  /**
109
154
  * Gets component info by name
110
155
  */
111
156
  export function getComponentInfo(componentName) {
112
- const components = getComponentList();
113
- return components.find((c) => c.name === componentName);
157
+ const normalizedInput = normalizeComponentName(componentName);
158
+ if (!normalizedInput) {
159
+ return undefined;
160
+ }
161
+ return getComponentInfoByNormalizedNameMap().get(normalizedInput);
114
162
  }
115
163
  /**
116
164
  * Reads the markdown documentation for a component
@@ -0,0 +1,37 @@
1
+ import type * as CSS from 'csstype';
2
+ import type { VariableScope } from '@figma/plugin-typings/plugin-api-standalone';
3
+ export type DesignTokenRecord = {
4
+ name: string;
5
+ description?: string;
6
+ type?: string;
7
+ deprecated?: boolean | string;
8
+ defaultValue: unknown;
9
+ overrideCount: number;
10
+ hasOverrides: boolean;
11
+ overridesByContext: Record<string, unknown>;
12
+ cssProperties?: Array<keyof CSS.Properties>;
13
+ figmaVariableScopes?: VariableScope[];
14
+ };
15
+ /**
16
+ * Override the tokens.json path (primarily for testing)
17
+ * @internal
18
+ */
19
+ export declare function setTokensPath(path: string | null): void;
20
+ /**
21
+ * Extracts the replacement token name from a deprecation message.
22
+ *
23
+ * Deprecation messages that include a replacement follow the format defined in
24
+ * `@splunk/themes` deprecated.ts: `Use \`replacementTokenName\``
25
+ * This format is also relied upon by `@splunk/eslint-plugin-splunk-ui` for auto-fixing.
26
+ */
27
+ export declare function extractReplacementTokenName(deprecated: boolean | string | undefined): string | undefined;
28
+ export declare function isDeprecatedToken(token: DesignTokenRecord): boolean;
29
+ export declare function getDesignTokenList(): DesignTokenRecord[];
30
+ export declare function getDesignTokenInfo(tokenName: string): DesignTokenRecord | undefined;
31
+ /**
32
+ * Search for design tokens matching the given query.
33
+ *
34
+ * Never returns deprecated tokens — use `getDesignTokenInfo` for direct lookup by name
35
+ * if you need to retrieve a token regardless of its deprecation status.
36
+ */
37
+ export declare function findDesignTokens(query: string): DesignTokenRecord[];