@splunk/splunk-ui-mcp 0.1.0 → 0.2.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 (33) hide show
  1. package/README.md +10 -3
  2. package/lib/constants/versions.js +3 -3
  3. package/lib/index.js +6 -4
  4. package/lib/resources/components.js +12 -6
  5. package/lib/resources/design-tokens.d.ts +17 -0
  6. package/lib/resources/design-tokens.js +113 -0
  7. package/lib/tools/find_component.d.ts +19 -0
  8. package/lib/tools/find_component.js +93 -0
  9. package/lib/tools/find_design_token.d.ts +16 -0
  10. package/lib/tools/find_design_token.js +126 -0
  11. package/lib/tools/{find_icons.d.ts → find_icon.d.ts} +2 -2
  12. package/lib/tools/{find_icons.js → find_icon.js} +5 -5
  13. package/lib/utils/component-catalog.d.ts +19 -2
  14. package/lib/utils/component-catalog.js +77 -29
  15. package/lib/utils/design-token-catalog.d.ts +33 -0
  16. package/lib/utils/design-token-catalog.js +158 -0
  17. package/lib/utils/package-assets.d.ts +8 -0
  18. package/lib/utils/package-assets.js +35 -0
  19. package/package.json +14 -10
  20. package/lib/resources/tests/components.unit.d.ts +0 -1
  21. package/lib/resources/tests/components.unit.js +0 -133
  22. package/lib/resources/tests/icons.unit.d.ts +0 -1
  23. package/lib/resources/tests/icons.unit.js +0 -161
  24. package/lib/tools/get_component_docs.d.ts +0 -19
  25. package/lib/tools/get_component_docs.js +0 -82
  26. package/lib/tools/tests/find_icons.unit.d.ts +0 -1
  27. package/lib/tools/tests/find_icons.unit.js +0 -149
  28. package/lib/tools/tests/get_component_docs.unit.d.ts +0 -1
  29. package/lib/tools/tests/get_component_docs.unit.js +0 -131
  30. package/lib/tools/tests/requirements.unit.d.ts +0 -1
  31. package/lib/tools/tests/requirements.unit.js +0 -34
  32. package/lib/utils/tests/component-catalog.unit.d.ts +0 -1
  33. package/lib/utils/tests/component-catalog.unit.js +0 -144
@@ -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,33 @@
1
+ export type DesignTokenRecord = {
2
+ name: string;
3
+ description?: string;
4
+ type?: string;
5
+ deprecated?: boolean | string;
6
+ defaultValue: unknown;
7
+ overrideCount: number;
8
+ hasOverrides: boolean;
9
+ overridesByContext: Record<string, unknown>;
10
+ };
11
+ /**
12
+ * Override the tokens.json path (primarily for testing)
13
+ * @internal
14
+ */
15
+ export declare function setTokensPath(path: string | null): void;
16
+ /**
17
+ * Extracts the replacement token name from a deprecation message.
18
+ *
19
+ * Deprecation messages that include a replacement follow the format defined in
20
+ * `@splunk/themes` deprecated.ts: `Use \`replacementTokenName\``
21
+ * This format is also relied upon by `@splunk/eslint-plugin-splunk-ui` for auto-fixing.
22
+ */
23
+ export declare function extractReplacementTokenName(deprecated: boolean | string | undefined): string | undefined;
24
+ export declare function isDeprecatedToken(token: DesignTokenRecord): boolean;
25
+ export declare function getDesignTokenList(): DesignTokenRecord[];
26
+ export declare function getDesignTokenInfo(tokenName: string): DesignTokenRecord | undefined;
27
+ /**
28
+ * Search for design tokens matching the given query.
29
+ *
30
+ * Never returns deprecated tokens — use `getDesignTokenInfo` for direct lookup by name
31
+ * if you need to retrieve a token regardless of its deprecation status.
32
+ */
33
+ export declare function findDesignTokens(query: string): DesignTokenRecord[];
@@ -0,0 +1,158 @@
1
+ import { readFileSync } from 'fs';
2
+ import { addSearchData, search } from '@splunk/ui-utils/search.js';
3
+ import { resolvePackageAssetPath } from "./package-assets.js";
4
+ let designTokenCache = null;
5
+ let tokenSearchIndex = null;
6
+ let tokensPath = null;
7
+ /**
8
+ * Override the tokens.json path (primarily for testing)
9
+ * @internal
10
+ */
11
+ export function setTokensPath(path) {
12
+ tokensPath = path;
13
+ designTokenCache = null;
14
+ tokenSearchIndex = null;
15
+ }
16
+ function resolveTokensPath() {
17
+ if (tokensPath) {
18
+ return tokensPath;
19
+ }
20
+ try {
21
+ tokensPath = resolvePackageAssetPath('@splunk/themes', 'tokens.json');
22
+ return tokensPath;
23
+ }
24
+ catch (error) {
25
+ throw new Error('@splunk/themes tokens.json not found. ' +
26
+ 'Either install it in your project (npm install @splunk/themes) ' +
27
+ 'or ensure the MCP was installed with peer dependencies.\n' +
28
+ `Error: ${error.message}`);
29
+ }
30
+ }
31
+ const upsertTokenRecord = (tokenMap, tokenName, token, options = {}) => {
32
+ const existing = tokenMap.get(tokenName);
33
+ const next = existing ?? {
34
+ name: tokenName,
35
+ defaultValue: token.$value,
36
+ overrideCount: 0,
37
+ hasOverrides: false,
38
+ overridesByContext: {},
39
+ };
40
+ if (next.description == null && token.$description) {
41
+ next.description = token.$description;
42
+ }
43
+ if (next.type == null && token.$type) {
44
+ next.type = token.$type;
45
+ }
46
+ if (next.deprecated == null &&
47
+ token.$deprecated != null &&
48
+ (existing == null || options.allowDeprecatedOverride !== false)) {
49
+ next.deprecated = token.$deprecated;
50
+ }
51
+ // Use the first available value as default fallback if a base token isn't present.
52
+ if (next.defaultValue == null && token.$value != null) {
53
+ next.defaultValue = token.$value;
54
+ }
55
+ tokenMap.set(tokenName, next);
56
+ return next;
57
+ };
58
+ const getBaseSources = (resolverDoc) => {
59
+ const explicitBase = resolverDoc.sets?.base?.sources;
60
+ if (explicitBase) {
61
+ return explicitBase;
62
+ }
63
+ const firstSet = Object.values(resolverDoc.sets ?? {})[0];
64
+ return firstSet?.sources ?? [];
65
+ };
66
+ const parseDesignTokens = (resolverDoc) => {
67
+ const tokenMap = new Map();
68
+ getBaseSources(resolverDoc).forEach((tokenSource) => {
69
+ Object.entries(tokenSource).forEach(([tokenName, token]) => {
70
+ upsertTokenRecord(tokenMap, tokenName, token);
71
+ });
72
+ });
73
+ Object.values(resolverDoc.modifiers ?? {}).forEach((modifier) => {
74
+ Object.entries(modifier.contexts ?? {}).forEach(([contextName, tokenSources]) => {
75
+ tokenSources.forEach((tokenSource) => {
76
+ Object.entries(tokenSource).forEach(([tokenName, token]) => {
77
+ const record = upsertTokenRecord(tokenMap, tokenName, token, {
78
+ allowDeprecatedOverride: false,
79
+ });
80
+ if (!(contextName in record.overridesByContext)) {
81
+ record.overridesByContext[contextName] = token.$value;
82
+ record.overrideCount += 1;
83
+ record.hasOverrides = true;
84
+ }
85
+ });
86
+ });
87
+ });
88
+ });
89
+ return Array.from(tokenMap.values()).sort((a, b) => a.name.localeCompare(b.name));
90
+ };
91
+ const buildSearchIndex = (tokens) => addSearchData(tokens.map((token) => ({
92
+ name: token.name,
93
+ description: token.description,
94
+ keywords: [token.type, token.deprecated != null ? 'deprecated' : '', 'design token']
95
+ .filter(Boolean)
96
+ .join(' '),
97
+ token,
98
+ })));
99
+ const getCachedTokens = () => {
100
+ if (!designTokenCache) {
101
+ const resolvedTokensPath = resolveTokensPath();
102
+ const rawContent = readFileSync(resolvedTokensPath, 'utf-8');
103
+ let tokenSpec;
104
+ try {
105
+ tokenSpec = JSON.parse(rawContent);
106
+ }
107
+ catch (error) {
108
+ throw new Error(`Failed to parse tokens.json at "${resolvedTokensPath}". ` +
109
+ `The file may be malformed or from an incompatible version of @splunk/themes.\n` +
110
+ `Error: ${error.message}`);
111
+ }
112
+ designTokenCache = parseDesignTokens(tokenSpec);
113
+ tokenSearchIndex = buildSearchIndex(designTokenCache);
114
+ }
115
+ return designTokenCache;
116
+ };
117
+ /**
118
+ * Extracts the replacement token name from a deprecation message.
119
+ *
120
+ * Deprecation messages that include a replacement follow the format defined in
121
+ * `@splunk/themes` deprecated.ts: `Use \`replacementTokenName\``
122
+ * This format is also relied upon by `@splunk/eslint-plugin-splunk-ui` for auto-fixing.
123
+ */
124
+ export function extractReplacementTokenName(deprecated) {
125
+ if (typeof deprecated !== 'string') {
126
+ return undefined;
127
+ }
128
+ const match = deprecated.match(/^Use `([^`]+)`$/);
129
+ return match?.[1];
130
+ }
131
+ export function isDeprecatedToken(token) {
132
+ return (token.deprecated === true ||
133
+ (typeof token.deprecated === 'string' && token.deprecated.trim().length > 0));
134
+ }
135
+ export function getDesignTokenList() {
136
+ return getCachedTokens();
137
+ }
138
+ export function getDesignTokenInfo(tokenName) {
139
+ return getCachedTokens().find((token) => token.name === tokenName);
140
+ }
141
+ /**
142
+ * Search for design tokens matching the given query.
143
+ *
144
+ * Never returns deprecated tokens — use `getDesignTokenInfo` for direct lookup by name
145
+ * if you need to retrieve a token regardless of its deprecation status.
146
+ */
147
+ export function findDesignTokens(query) {
148
+ const trimmed = query.trim();
149
+ if (!trimmed) {
150
+ return [];
151
+ }
152
+ const tokens = getCachedTokens();
153
+ if (!tokenSearchIndex) {
154
+ tokenSearchIndex = buildSearchIndex(tokens);
155
+ }
156
+ const results = search(trimmed, tokenSearchIndex);
157
+ return results.map((result) => result.token).filter((token) => !isDeprecatedToken(token));
158
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Resolves an asset file from a package.
3
+ *
4
+ * Resolution order:
5
+ * 1. User's project node_modules
6
+ * 2. MCP's bundled dependencies
7
+ */
8
+ export declare function resolvePackageAssetPath(packageName: string, assetPath: string): string;
@@ -0,0 +1,35 @@
1
+ import { createRequire } from 'module';
2
+ import { dirname, join } from 'path';
3
+ const resolverModes = ['project', 'bundled'];
4
+ const getPackageRootFrom = (packageName, mode) => {
5
+ const require = mode === 'project'
6
+ ? createRequire(join(process.cwd(), 'package.json'))
7
+ : createRequire(import.meta.url);
8
+ const packageJsonPath = require.resolve(`${packageName}/package.json`);
9
+ return dirname(packageJsonPath);
10
+ };
11
+ /**
12
+ * Resolves an asset file from a package.
13
+ *
14
+ * Resolution order:
15
+ * 1. User's project node_modules
16
+ * 2. MCP's bundled dependencies
17
+ */
18
+ export function resolvePackageAssetPath(packageName, assetPath) {
19
+ const errors = [];
20
+ let packageRoot = null;
21
+ resolverModes.find((mode) => {
22
+ try {
23
+ packageRoot = getPackageRootFrom(packageName, mode);
24
+ return true;
25
+ }
26
+ catch (error) {
27
+ errors.push(`${mode}: ${error.message}`);
28
+ return false;
29
+ }
30
+ });
31
+ if (packageRoot) {
32
+ return join(packageRoot, assetPath);
33
+ }
34
+ throw new Error(`Failed to resolve "${assetPath}" from ${packageName}. Tried project and bundled package resolution.\n${errors.join('\n')}`);
35
+ }
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@splunk/splunk-ui-mcp",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Model Context Protocol server for the Splunk UI Design System",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Splunk Inc.",
7
7
  "scripts": {
8
8
  "prebuild": "node scripts/generate-versions.js",
9
- "build": "tsc",
9
+ "build": "yarn types:build && tsc -p tsconfig.json",
10
10
  "docs": "NODE_ENV=production webpack --config docs.gen.webpack.config.cjs",
11
11
  "docs:publish": "eval $(splunk-docs-package docs) && artifact-ci publish generic $DOCS_GEN_OUTPUT_NAME $DOCS_GEN_REMOTE_PATH",
12
12
  "docs:publish:external": "eval $(splunk-docs-package docs-external --suffix=public) && artifact-ci publish generic $DOCS_GEN_OUTPUT_NAME $DOCS_GEN_REMOTE_PATH",
@@ -14,7 +14,8 @@
14
14
  "lint": "eslint src --ext \".ts,.tsx,.js,.jsx\"",
15
15
  "lint:ci": "yarn run lint -- -f junit -o test-reports/lint-results.xml",
16
16
  "start": "tsc --watch",
17
- "test": "vitest run"
17
+ "test": "vitest run",
18
+ "types:build": "tsc -p tsconfig.check.json"
18
19
  },
19
20
  "type": "module",
20
21
  "main": "lib/index.js",
@@ -23,21 +24,24 @@
23
24
  "splunk-ui-mcp": "lib/index.js"
24
25
  },
25
26
  "files": [
26
- "lib"
27
+ "lib",
28
+ "!lib/**/tests/**"
27
29
  ],
28
30
  "dependencies": {
29
- "@modelcontextprotocol/sdk": "^1.17.5",
31
+ "@modelcontextprotocol/sdk": "^1.26.0",
30
32
  "@splunk/ui-utils": "^1.12.0",
31
- "zod": "^3.0.0"
33
+ "zod": "^4.0.0"
32
34
  },
33
35
  "peerDependencies": {
34
- "@splunk/react-icons": "^5.8.0",
35
- "@splunk/react-ui": "^5.8.0"
36
+ "@splunk/react-icons": "^5.9.0",
37
+ "@splunk/react-ui": "^5.9.0",
38
+ "@splunk/themes": "^1.6.0"
36
39
  },
37
40
  "devDependencies": {
38
41
  "@splunk/eslint-config": "^5.0.0",
39
- "@splunk/react-icons": "^5.8.0",
40
- "@splunk/react-ui": "^5.8.0",
42
+ "@splunk/react-icons": "^5.9.0",
43
+ "@splunk/react-ui": "^5.9.0",
44
+ "@splunk/themes": "^1.6.0",
41
45
  "@types/node": "^22.0.0",
42
46
  "@typescript-eslint/eslint-plugin": "^8.29.1",
43
47
  "@typescript-eslint/parser": "^8.29.1",
@@ -1 +0,0 @@
1
- export {};
@@ -1,133 +0,0 @@
1
- import { beforeEach, describe, expect, it, vi } from 'vitest';
2
- const MOCK_COMPONENT_LIST = [
3
- {
4
- name: 'Button',
5
- filename: 'Button.md',
6
- category: 'Base',
7
- },
8
- {
9
- name: 'Card Layout',
10
- filename: 'Card Layout.md',
11
- category: 'Base',
12
- },
13
- {
14
- name: 'Text',
15
- filename: 'Text.md',
16
- category: 'Data Entry',
17
- },
18
- ];
19
- const MOCK_BUTTON_DOCS = `# Button
20
-
21
- ## Overview
22
-
23
- A button component for triggering actions.
24
-
25
- ## Examples
26
-
27
- \`\`\`typescript
28
- import Button from '@splunk/react-ui/Button';
29
- \`\`\`
30
- `;
31
- // Mock the component catalog module
32
- vi.mock('../../utils/component-catalog.ts', () => ({
33
- getComponentList: vi.fn(() => MOCK_COMPONENT_LIST),
34
- getComponentInfo: vi.fn((name) => MOCK_COMPONENT_LIST.find((c) => c.name === name)),
35
- getComponentDocs: vi.fn((name) => {
36
- if (name === 'Button') {
37
- return MOCK_BUTTON_DOCS;
38
- }
39
- throw new Error(`Component "${name}" not found in catalog`);
40
- }),
41
- }));
42
- describe('createComponentResourceUri', () => {
43
- it('builds an encoded MCP URI for the provided component name', async () => {
44
- const { createComponentResourceUri, COMPONENT_RESOURCE_URI_BASE } = await import("../components.js");
45
- expect(createComponentResourceUri('Button')).toBe(`${COMPONENT_RESOURCE_URI_BASE}/Button`);
46
- expect(createComponentResourceUri('Card Layout')).toBe(`${COMPONENT_RESOURCE_URI_BASE}/Card%20Layout`);
47
- });
48
- });
49
- describe('component resources', () => {
50
- beforeEach(() => {
51
- vi.resetModules();
52
- });
53
- describe('createComponentResourceLink', () => {
54
- it('includes catalog metadata when the component exists', async () => {
55
- const { createComponentResourceLink, createComponentResourceUri } = await import("../components.js");
56
- const link = createComponentResourceLink('Button');
57
- expect(link).toMatchObject({
58
- type: 'resource_link',
59
- name: 'Button',
60
- title: 'Button',
61
- uri: createComponentResourceUri('Button'),
62
- description: 'Base component',
63
- mimeType: 'text/markdown',
64
- });
65
- });
66
- it('creates a resource link even if the component is unknown', async () => {
67
- const { createComponentResourceLink, createComponentResourceUri } = await import("../components.js");
68
- const link = createComponentResourceLink('Unknown');
69
- expect(link).toMatchObject({
70
- type: 'resource_link',
71
- name: 'Unknown',
72
- title: 'Unknown',
73
- uri: createComponentResourceUri('Unknown'),
74
- mimeType: 'text/markdown',
75
- });
76
- });
77
- });
78
- describe('component resource handler', () => {
79
- it('reads component documentation by name', async () => {
80
- const componentResource = (await import("../components.js")).default;
81
- const url = new URL('mcp://splunk-ui/components/Button');
82
- const result = componentResource.handler(url, {
83
- componentName: 'Button',
84
- });
85
- expect(result.contents).toHaveLength(1);
86
- expect(result.contents[0]).toMatchObject({
87
- mimeType: 'text/markdown',
88
- text: MOCK_BUTTON_DOCS,
89
- });
90
- });
91
- it('extracts component name from URL path if param is missing', async () => {
92
- const componentResource = (await import("../components.js")).default;
93
- const url = new URL('mcp://splunk-ui/components/Button');
94
- const result = componentResource.handler(url, {});
95
- expect(result.contents).toHaveLength(1);
96
- expect(result.contents[0]).toMatchObject({
97
- mimeType: 'text/markdown',
98
- text: MOCK_BUTTON_DOCS,
99
- });
100
- });
101
- it('throws error when component name is missing', async () => {
102
- const componentResource = (await import("../components.js")).default;
103
- // URL with no component in path
104
- const url = new URL('mcp://splunk-ui/components');
105
- expect(() => componentResource.handler(url, {})).toThrow('Component name missing in resource request');
106
- });
107
- it('throws error for unknown component', async () => {
108
- const componentResource = (await import("../components.js")).default;
109
- const url = new URL('mcp://splunk-ui/components/Unknown');
110
- expect(() => componentResource.handler(url, { componentName: 'Unknown' })).toThrow('Unknown component "Unknown"');
111
- });
112
- });
113
- describe('component resource template', () => {
114
- it('autocompletes component names', async () => {
115
- const componentResource = (await import("../components.js")).default;
116
- const complete = componentResource.template.completeCallback?.('componentName');
117
- const completions = (await complete?.('b')) ?? [];
118
- expect(completions).toEqual(['Button']);
119
- });
120
- it('autocompletes with partial matches', async () => {
121
- const componentResource = (await import("../components.js")).default;
122
- const complete = componentResource.template.completeCallback?.('componentName');
123
- const completions = (await complete?.('card')) ?? [];
124
- expect(completions).toContain('Card Layout');
125
- });
126
- it('returns empty array when no matches found for autocomplete', async () => {
127
- const componentResource = (await import("../components.js")).default;
128
- const complete = componentResource.template.completeCallback?.('componentName');
129
- const completions = (await complete?.('xyz')) ?? [];
130
- expect(completions).toEqual([]);
131
- });
132
- });
133
- });
@@ -1 +0,0 @@
1
- export {};