@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.
- package/README.md +10 -3
- package/lib/constants/versions.js +3 -3
- package/lib/index.js +6 -4
- package/lib/resources/components.js +12 -6
- package/lib/resources/design-tokens.d.ts +17 -0
- package/lib/resources/design-tokens.js +113 -0
- package/lib/tools/find_component.d.ts +19 -0
- package/lib/tools/find_component.js +93 -0
- package/lib/tools/find_design_token.d.ts +16 -0
- package/lib/tools/find_design_token.js +126 -0
- package/lib/tools/{find_icons.d.ts → find_icon.d.ts} +2 -2
- package/lib/tools/{find_icons.js → find_icon.js} +5 -5
- package/lib/utils/component-catalog.d.ts +19 -2
- package/lib/utils/component-catalog.js +77 -29
- package/lib/utils/design-token-catalog.d.ts +33 -0
- package/lib/utils/design-token-catalog.js +158 -0
- package/lib/utils/package-assets.d.ts +8 -0
- package/lib/utils/package-assets.js +35 -0
- package/package.json +14 -10
- package/lib/resources/tests/components.unit.d.ts +0 -1
- package/lib/resources/tests/components.unit.js +0 -133
- package/lib/resources/tests/icons.unit.d.ts +0 -1
- package/lib/resources/tests/icons.unit.js +0 -161
- package/lib/tools/get_component_docs.d.ts +0 -19
- package/lib/tools/get_component_docs.js +0 -82
- package/lib/tools/tests/find_icons.unit.d.ts +0 -1
- package/lib/tools/tests/find_icons.unit.js +0 -149
- package/lib/tools/tests/get_component_docs.unit.d.ts +0 -1
- package/lib/tools/tests/get_component_docs.unit.js +0 -131
- package/lib/tools/tests/requirements.unit.d.ts +0 -1
- package/lib/tools/tests/requirements.unit.js +0 -34
- package/lib/utils/tests/component-catalog.unit.d.ts +0 -1
- package/lib/utils/tests/component-catalog.unit.js +0 -144
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { readFileSync } from 'fs';
|
|
2
|
-
import { join,
|
|
3
|
-
import {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
97
|
-
componentCache = parseComponentCatalog();
|
|
98
|
-
}
|
|
101
|
+
componentCache ??= parseComponentCatalog();
|
|
99
102
|
return componentCache;
|
|
100
103
|
}
|
|
101
104
|
/**
|
|
102
|
-
*
|
|
105
|
+
* Returns the cached list of component names
|
|
103
106
|
*/
|
|
104
|
-
export function
|
|
105
|
-
|
|
106
|
-
return
|
|
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
|
|
113
|
-
|
|
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,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.
|
|
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.
|
|
31
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
30
32
|
"@splunk/ui-utils": "^1.12.0",
|
|
31
|
-
"zod": "^
|
|
33
|
+
"zod": "^4.0.0"
|
|
32
34
|
},
|
|
33
35
|
"peerDependencies": {
|
|
34
|
-
"@splunk/react-icons": "^5.
|
|
35
|
-
"@splunk/react-ui": "^5.
|
|
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.
|
|
40
|
-
"@splunk/react-ui": "^5.
|
|
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 {};
|