@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.
- package/README.md +19 -5
- package/lib/constants/versions.js +3 -3
- package/lib/index.js +16 -4
- package/lib/prompts/audit/audit-prompt.d.ts +2 -0
- package/lib/prompts/audit/audit-prompt.js +11 -0
- package/lib/prompts/audit/audit.d.ts +16 -0
- package/lib/prompts/audit/audit.js +36 -0
- package/lib/prompts/audit/index.d.ts +1 -0
- package/lib/prompts/audit/index.js +1 -0
- package/lib/prompts/build-ui/build-figma-design-prompt.d.ts +2 -0
- package/lib/prompts/build-ui/build-figma-design-prompt.js +9 -0
- package/lib/prompts/build-ui/build-ui-prompt.d.ts +2 -0
- package/lib/prompts/build-ui/build-ui-prompt.js +8 -0
- package/lib/prompts/build-ui/build-ui.d.ts +16 -0
- package/lib/prompts/build-ui/build-ui.js +62 -0
- package/lib/prompts/build-ui/index.d.ts +1 -0
- package/lib/prompts/build-ui/index.js +1 -0
- package/lib/prompts/create-scaffold/create-scaffold-prompt.d.ts +2 -0
- package/lib/prompts/create-scaffold/create-scaffold-prompt.js +34 -0
- package/lib/prompts/create-scaffold/create-scaffold.d.ts +16 -0
- package/lib/prompts/create-scaffold/create-scaffold.js +60 -0
- package/lib/prompts/create-scaffold/index.d.ts +1 -0
- package/lib/prompts/create-scaffold/index.js +1 -0
- package/lib/prompts/session-feedback/index.d.ts +1 -0
- package/lib/prompts/session-feedback/index.js +1 -0
- package/lib/prompts/session-feedback/session-feedback-prompt.d.ts +2 -0
- package/lib/prompts/session-feedback/session-feedback-prompt.js +113 -0
- package/lib/prompts/session-feedback/session-feedback.d.ts +11 -0
- package/lib/prompts/session-feedback/session-feedback.js +22 -0
- package/lib/prompts/types.d.ts +8 -0
- package/lib/resources/components.js +12 -6
- package/lib/resources/design-tokens.d.ts +17 -0
- package/lib/resources/design-tokens.js +115 -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 +134 -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 +37 -0
- package/lib/utils/design-token-catalog.js +166 -0
- package/lib/utils/package-assets.d.ts +8 -0
- package/lib/utils/package-assets.js +35 -0
- package/package.json +16 -10
- 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
- /package/lib/{resources/tests/components.unit.d.ts → prompts/types.js} +0 -0
|
@@ -0,0 +1,166 @@
|
|
|
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
|
+
const splunkExt = token.$extensions?.['com.splunk.splunkui'];
|
|
56
|
+
next.cssProperties ??= splunkExt?.cssProperties;
|
|
57
|
+
next.figmaVariableScopes ??= splunkExt?.figmaVariableScopes;
|
|
58
|
+
tokenMap.set(tokenName, next);
|
|
59
|
+
return next;
|
|
60
|
+
};
|
|
61
|
+
const getBaseSources = (resolverDoc) => {
|
|
62
|
+
const explicitBase = resolverDoc.sets?.base?.sources;
|
|
63
|
+
if (explicitBase) {
|
|
64
|
+
return explicitBase;
|
|
65
|
+
}
|
|
66
|
+
const firstSet = Object.values(resolverDoc.sets ?? {})[0];
|
|
67
|
+
return firstSet?.sources ?? [];
|
|
68
|
+
};
|
|
69
|
+
const parseDesignTokens = (resolverDoc) => {
|
|
70
|
+
const tokenMap = new Map();
|
|
71
|
+
getBaseSources(resolverDoc).forEach((tokenSource) => {
|
|
72
|
+
Object.entries(tokenSource).forEach(([tokenName, token]) => {
|
|
73
|
+
upsertTokenRecord(tokenMap, tokenName, token);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
Object.values(resolverDoc.modifiers ?? {}).forEach((modifier) => {
|
|
77
|
+
Object.entries(modifier.contexts ?? {}).forEach(([contextName, tokenSources]) => {
|
|
78
|
+
tokenSources.forEach((tokenSource) => {
|
|
79
|
+
Object.entries(tokenSource).forEach(([tokenName, token]) => {
|
|
80
|
+
const record = upsertTokenRecord(tokenMap, tokenName, token, {
|
|
81
|
+
allowDeprecatedOverride: false,
|
|
82
|
+
});
|
|
83
|
+
if (!(contextName in record.overridesByContext)) {
|
|
84
|
+
record.overridesByContext[contextName] = token.$value;
|
|
85
|
+
record.overrideCount += 1;
|
|
86
|
+
record.hasOverrides = true;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
return Array.from(tokenMap.values()).sort((a, b) => a.name.localeCompare(b.name));
|
|
93
|
+
};
|
|
94
|
+
const buildSearchIndex = (tokens) => addSearchData(tokens.map((token) => ({
|
|
95
|
+
name: token.name,
|
|
96
|
+
description: token.description,
|
|
97
|
+
keywords: [
|
|
98
|
+
token.type,
|
|
99
|
+
token.deprecated != null ? 'deprecated' : '',
|
|
100
|
+
'design token',
|
|
101
|
+
...(token.cssProperties ?? []),
|
|
102
|
+
]
|
|
103
|
+
.filter(Boolean)
|
|
104
|
+
.join(' '),
|
|
105
|
+
token,
|
|
106
|
+
})));
|
|
107
|
+
const getCachedTokens = () => {
|
|
108
|
+
if (!designTokenCache) {
|
|
109
|
+
const resolvedTokensPath = resolveTokensPath();
|
|
110
|
+
const rawContent = readFileSync(resolvedTokensPath, 'utf-8');
|
|
111
|
+
let tokenSpec;
|
|
112
|
+
try {
|
|
113
|
+
tokenSpec = JSON.parse(rawContent);
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
throw new Error(`Failed to parse tokens.json at "${resolvedTokensPath}". ` +
|
|
117
|
+
`The file may be malformed or from an incompatible version of @splunk/themes.\n` +
|
|
118
|
+
`Error: ${error.message}`);
|
|
119
|
+
}
|
|
120
|
+
designTokenCache = parseDesignTokens(tokenSpec);
|
|
121
|
+
tokenSearchIndex = buildSearchIndex(designTokenCache);
|
|
122
|
+
}
|
|
123
|
+
return designTokenCache;
|
|
124
|
+
};
|
|
125
|
+
/**
|
|
126
|
+
* Extracts the replacement token name from a deprecation message.
|
|
127
|
+
*
|
|
128
|
+
* Deprecation messages that include a replacement follow the format defined in
|
|
129
|
+
* `@splunk/themes` deprecated.ts: `Use \`replacementTokenName\``
|
|
130
|
+
* This format is also relied upon by `@splunk/eslint-plugin-splunk-ui` for auto-fixing.
|
|
131
|
+
*/
|
|
132
|
+
export function extractReplacementTokenName(deprecated) {
|
|
133
|
+
if (typeof deprecated !== 'string') {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
const match = deprecated.match(/^Use `([^`]+)`$/);
|
|
137
|
+
return match?.[1];
|
|
138
|
+
}
|
|
139
|
+
export function isDeprecatedToken(token) {
|
|
140
|
+
return (token.deprecated === true ||
|
|
141
|
+
(typeof token.deprecated === 'string' && token.deprecated.trim().length > 0));
|
|
142
|
+
}
|
|
143
|
+
export function getDesignTokenList() {
|
|
144
|
+
return getCachedTokens();
|
|
145
|
+
}
|
|
146
|
+
export function getDesignTokenInfo(tokenName) {
|
|
147
|
+
return getCachedTokens().find((token) => token.name === tokenName);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Search for design tokens matching the given query.
|
|
151
|
+
*
|
|
152
|
+
* Never returns deprecated tokens — use `getDesignTokenInfo` for direct lookup by name
|
|
153
|
+
* if you need to retrieve a token regardless of its deprecation status.
|
|
154
|
+
*/
|
|
155
|
+
export function findDesignTokens(query) {
|
|
156
|
+
const trimmed = query.trim();
|
|
157
|
+
if (!trimmed) {
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
const tokens = getCachedTokens();
|
|
161
|
+
if (!tokenSearchIndex) {
|
|
162
|
+
tokenSearchIndex = buildSearchIndex(tokens);
|
|
163
|
+
}
|
|
164
|
+
const results = search(trimmed, tokenSearchIndex);
|
|
165
|
+
return results.map((result) => result.token).filter((token) => !isDeprecatedToken(token));
|
|
166
|
+
}
|
|
@@ -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.3.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,25 +24,30 @@
|
|
|
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": {
|
|
41
|
+
"@figma/plugin-typings": "^1.123.0",
|
|
38
42
|
"@splunk/eslint-config": "^5.0.0",
|
|
39
|
-
"@splunk/react-icons": "^5.
|
|
40
|
-
"@splunk/react-ui": "^5.
|
|
43
|
+
"@splunk/react-icons": "^5.9.0",
|
|
44
|
+
"@splunk/react-ui": "^5.9.0",
|
|
45
|
+
"@splunk/themes": "^1.6.0",
|
|
41
46
|
"@types/node": "^22.0.0",
|
|
42
47
|
"@typescript-eslint/eslint-plugin": "^8.29.1",
|
|
43
48
|
"@typescript-eslint/parser": "^8.29.1",
|
|
44
49
|
"@vitest/coverage-v8": "^3.2.4",
|
|
50
|
+
"csstype": "^2.6.10",
|
|
45
51
|
"eslint": "^8.57.1",
|
|
46
52
|
"eslint-config-airbnb": "^19.0.4",
|
|
47
53
|
"eslint-config-prettier": "^9.1.0",
|
|
@@ -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 {};
|
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
const ICON_CATALOG_FIXTURE = vi.hoisted(() => [
|
|
3
|
-
{
|
|
4
|
-
name: 'SearchIcon',
|
|
5
|
-
category: 'Interface',
|
|
6
|
-
description: 'Use for search interactions.',
|
|
7
|
-
keywords: 'search, magnify, find',
|
|
8
|
-
variants: ['default', 'filled'],
|
|
9
|
-
},
|
|
10
|
-
{
|
|
11
|
-
name: 'AlertIcon',
|
|
12
|
-
keywords: 'alert, warning',
|
|
13
|
-
},
|
|
14
|
-
]);
|
|
15
|
-
vi.mock('@splunk/react-icons/icon-catalog.json', () => ({
|
|
16
|
-
default: ICON_CATALOG_FIXTURE,
|
|
17
|
-
}));
|
|
18
|
-
describe('createIconResourceUri', () => {
|
|
19
|
-
it('builds an encoded MCP URI for the provided icon name', async () => {
|
|
20
|
-
const { createIconResourceUri, ICON_RESOURCE_URI_BASE } = await import("../icons.js");
|
|
21
|
-
expect(createIconResourceUri('SimpleIcon')).toBe(`${ICON_RESOURCE_URI_BASE}/SimpleIcon`);
|
|
22
|
-
expect(createIconResourceUri('Icon With Spaces')).toBe(`${ICON_RESOURCE_URI_BASE}/Icon%20With%20Spaces`);
|
|
23
|
-
expect(createIconResourceUri('Icon/Separators')).toBe(`${ICON_RESOURCE_URI_BASE}/Icon%2FSeparators`);
|
|
24
|
-
});
|
|
25
|
-
});
|
|
26
|
-
describe('icon resources', () => {
|
|
27
|
-
beforeEach(() => {
|
|
28
|
-
vi.resetModules();
|
|
29
|
-
});
|
|
30
|
-
describe('createIconResourceLink', () => {
|
|
31
|
-
it('includes catalog metadata when the icon exists', async () => {
|
|
32
|
-
const { createIconResourceLink, createIconResourceUri } = await import("../icons.js");
|
|
33
|
-
const link = createIconResourceLink('SearchIcon');
|
|
34
|
-
expect(link).toMatchObject({
|
|
35
|
-
type: 'resource_link',
|
|
36
|
-
name: 'SearchIcon',
|
|
37
|
-
title: 'SearchIcon',
|
|
38
|
-
uri: createIconResourceUri('SearchIcon'),
|
|
39
|
-
description: 'Use for search interactions.',
|
|
40
|
-
mimeType: 'application/json',
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
it('falls back to minimal metadata when the icon is unknown', async () => {
|
|
44
|
-
const { createIconResourceLink, createIconResourceUri } = await import("../icons.js");
|
|
45
|
-
const link = createIconResourceLink('MissingIcon');
|
|
46
|
-
expect(link).toMatchObject({
|
|
47
|
-
type: 'resource_link',
|
|
48
|
-
name: 'MissingIcon',
|
|
49
|
-
title: 'MissingIcon',
|
|
50
|
-
uri: createIconResourceUri('MissingIcon'),
|
|
51
|
-
description: undefined,
|
|
52
|
-
mimeType: 'application/json',
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
describe('getIconResourceDetails', () => {
|
|
57
|
-
it('returns structured details for known icons', async () => {
|
|
58
|
-
const { getIconResourceDetails } = await import("../icons.js");
|
|
59
|
-
const details = getIconResourceDetails('SearchIcon');
|
|
60
|
-
expect(details).toMatchObject({
|
|
61
|
-
name: 'SearchIcon',
|
|
62
|
-
importPath: '@splunk/react-icons/SearchIcon',
|
|
63
|
-
category: 'Interface',
|
|
64
|
-
description: 'Use for search interactions.',
|
|
65
|
-
keywords: 'search, magnify, find',
|
|
66
|
-
variants: ['default', 'filled'],
|
|
67
|
-
});
|
|
68
|
-
expect(details?.usageExample).toContain('SearchIcon');
|
|
69
|
-
});
|
|
70
|
-
it('returns undefined when the icon is missing', async () => {
|
|
71
|
-
const { getIconResourceDetails } = await import("../icons.js");
|
|
72
|
-
expect(getIconResourceDetails('MissingIcon')).toBeUndefined();
|
|
73
|
-
});
|
|
74
|
-
it('accepts catalog entries directly', async () => {
|
|
75
|
-
const { getIconResourceDetails } = await import("../icons.js");
|
|
76
|
-
const details = getIconResourceDetails({
|
|
77
|
-
name: 'AlertIcon',
|
|
78
|
-
key: './AlertIcon',
|
|
79
|
-
keywords: 'alert, warning',
|
|
80
|
-
variants: ['default'],
|
|
81
|
-
description: 'An alert icon.',
|
|
82
|
-
category: '',
|
|
83
|
-
});
|
|
84
|
-
expect(details).toMatchObject({
|
|
85
|
-
name: 'AlertIcon',
|
|
86
|
-
importPath: '@splunk/react-icons/AlertIcon',
|
|
87
|
-
});
|
|
88
|
-
expect(details.usageExample).toContain('AlertIcon');
|
|
89
|
-
});
|
|
90
|
-
});
|
|
91
|
-
describe('iconResource', () => {
|
|
92
|
-
const setup = async () => {
|
|
93
|
-
const { default: iconResource, createIconResourceUri } = await import("../icons.js");
|
|
94
|
-
return {
|
|
95
|
-
iconResource,
|
|
96
|
-
createIconResourceUri,
|
|
97
|
-
};
|
|
98
|
-
};
|
|
99
|
-
it('provides the expected metadata configuration', async () => {
|
|
100
|
-
const { iconResource } = await setup();
|
|
101
|
-
expect(iconResource.name).toBe('splunk-ui-icons');
|
|
102
|
-
expect(iconResource.config).toMatchObject({
|
|
103
|
-
title: expect.stringContaining('Icon Catalog'),
|
|
104
|
-
mimeType: 'application/json',
|
|
105
|
-
});
|
|
106
|
-
});
|
|
107
|
-
it('lists icons from the catalog', async () => {
|
|
108
|
-
const { iconResource } = await setup();
|
|
109
|
-
const { template } = iconResource;
|
|
110
|
-
const listResult = await template.listCallback?.();
|
|
111
|
-
expect(listResult).toMatchObject({
|
|
112
|
-
resources: expect.arrayContaining([
|
|
113
|
-
expect.objectContaining({ name: 'SearchIcon' }),
|
|
114
|
-
expect.objectContaining({ name: 'AlertIcon' }),
|
|
115
|
-
]),
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
it('suggests icon names using the completion handler', async () => {
|
|
119
|
-
const { iconResource } = await setup();
|
|
120
|
-
const complete = iconResource.template.completeCallback?.('iconName');
|
|
121
|
-
const completions = (await complete?.('se')) ?? [];
|
|
122
|
-
expect(completions).toContain('SearchIcon');
|
|
123
|
-
expect(completions).not.toContain('AlertIcon');
|
|
124
|
-
});
|
|
125
|
-
it('reads icon details using request variables', async () => {
|
|
126
|
-
const { iconResource, createIconResourceUri } = await setup();
|
|
127
|
-
expect(typeof iconResource.handler).toBe('function');
|
|
128
|
-
const readResult = await iconResource.handler(new URL(createIconResourceUri('AlertIcon')), { iconName: 'AlertIcon' });
|
|
129
|
-
const [content] = readResult.contents;
|
|
130
|
-
expect(content).toMatchObject({
|
|
131
|
-
uri: createIconResourceUri('AlertIcon'),
|
|
132
|
-
mimeType: 'application/json',
|
|
133
|
-
});
|
|
134
|
-
const parsed = JSON.parse(String(content.text ?? '{}'));
|
|
135
|
-
expect(parsed).toMatchObject({
|
|
136
|
-
name: 'AlertIcon',
|
|
137
|
-
importPath: '@splunk/react-icons/AlertIcon',
|
|
138
|
-
keywords: 'alert, warning',
|
|
139
|
-
});
|
|
140
|
-
expect(parsed.usageExample).toContain('AlertIcon');
|
|
141
|
-
// Ensure the template reference is exercised for coverage.
|
|
142
|
-
expect(iconResource.template).toBeDefined();
|
|
143
|
-
});
|
|
144
|
-
it('derives the icon name from the URI when variables are absent', async () => {
|
|
145
|
-
const { iconResource, createIconResourceUri } = await setup();
|
|
146
|
-
const readWithoutVariable = (await iconResource.handler(new URL(createIconResourceUri('SearchIcon')), {}));
|
|
147
|
-
const [content] = readWithoutVariable.contents;
|
|
148
|
-
expect(JSON.parse(String(content.text))).toMatchObject({ name: 'SearchIcon' });
|
|
149
|
-
});
|
|
150
|
-
it('throws when the icon cannot be resolved from variables or the URI', async () => {
|
|
151
|
-
const { iconResource } = await setup();
|
|
152
|
-
expect(() => iconResource.handler(new URL('mcp://splunk-ui'), {})).toThrowError('Icon name missing in resource request: mcp://splunk-ui');
|
|
153
|
-
});
|
|
154
|
-
it('throws when the requested icon is not found in the catalog', async () => {
|
|
155
|
-
const { iconResource, createIconResourceUri } = await setup();
|
|
156
|
-
expect(() => iconResource.handler(new URL(createIconResourceUri('MissingIcon')), {
|
|
157
|
-
iconName: 'MissingIcon',
|
|
158
|
-
})).toThrowError('Unknown icon "MissingIcon"');
|
|
159
|
-
});
|
|
160
|
-
});
|
|
161
|
-
});
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
-
import { z } from 'zod';
|
|
3
|
-
/**
|
|
4
|
-
* Tool to get documentation for a specific React UI component
|
|
5
|
-
*/
|
|
6
|
-
declare const getComponentDocsTool: {
|
|
7
|
-
name: string;
|
|
8
|
-
config: {
|
|
9
|
-
title: string;
|
|
10
|
-
description: string;
|
|
11
|
-
inputSchema: {
|
|
12
|
-
componentName: z.ZodString;
|
|
13
|
-
};
|
|
14
|
-
};
|
|
15
|
-
handler: ({ componentName }: {
|
|
16
|
-
componentName?: string;
|
|
17
|
-
}) => CallToolResult;
|
|
18
|
-
};
|
|
19
|
-
export default getComponentDocsTool;
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import { createComponentResourceLink } from "../resources/components.js";
|
|
3
|
-
import { componentExists, getComponentDocs, getComponentList } from "../utils/component-catalog.js";
|
|
4
|
-
/**
|
|
5
|
-
* Tool to get documentation for a specific React UI component
|
|
6
|
-
*/
|
|
7
|
-
const getComponentDocsTool = {
|
|
8
|
-
name: 'get_component_docs',
|
|
9
|
-
config: {
|
|
10
|
-
title: 'Get Component Documentation',
|
|
11
|
-
description: 'Retrieves the full documentation for a specific @splunk/react-ui component. ' +
|
|
12
|
-
'Includes overview, usage guidelines, examples, API documentation, and accessibility information.',
|
|
13
|
-
inputSchema: {
|
|
14
|
-
componentName: z
|
|
15
|
-
.string()
|
|
16
|
-
.describe('The exact name of the component (e.g., "Button", "Card Layout", "Text"). ' +
|
|
17
|
-
'Case-sensitive. Use the component name as it appears in the catalog.'),
|
|
18
|
-
},
|
|
19
|
-
},
|
|
20
|
-
handler: ({ componentName }) => {
|
|
21
|
-
if (!componentName || componentName.trim().length === 0) {
|
|
22
|
-
return {
|
|
23
|
-
content: [
|
|
24
|
-
{
|
|
25
|
-
type: 'text',
|
|
26
|
-
text: 'Component name is required. Please provide a component name.',
|
|
27
|
-
},
|
|
28
|
-
],
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
// Check if component exists
|
|
32
|
-
if (!componentExists(componentName)) {
|
|
33
|
-
const components = getComponentList();
|
|
34
|
-
const availableNames = components.map((c) => c.name).slice(0, 10);
|
|
35
|
-
return {
|
|
36
|
-
content: [
|
|
37
|
-
{
|
|
38
|
-
type: 'text',
|
|
39
|
-
text: `Component "${componentName}" not found.\n\n` +
|
|
40
|
-
`Available components include: ${availableNames.join(', ')}, and more.\n\n` +
|
|
41
|
-
'Component names are case-sensitive. Please check the spelling and try again.',
|
|
42
|
-
},
|
|
43
|
-
],
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
// Get the documentation
|
|
47
|
-
try {
|
|
48
|
-
const docs = getComponentDocs(componentName);
|
|
49
|
-
return {
|
|
50
|
-
content: [
|
|
51
|
-
{
|
|
52
|
-
type: 'text',
|
|
53
|
-
text: docs,
|
|
54
|
-
},
|
|
55
|
-
createComponentResourceLink(componentName),
|
|
56
|
-
],
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
catch (error) {
|
|
60
|
-
return {
|
|
61
|
-
content: [
|
|
62
|
-
{
|
|
63
|
-
type: 'text',
|
|
64
|
-
text: `Failed to retrieve documentation for "${componentName}": ${error.message}`,
|
|
65
|
-
},
|
|
66
|
-
],
|
|
67
|
-
isError: true,
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
},
|
|
71
|
-
};
|
|
72
|
-
export default getComponentDocsTool;
|
|
73
|
-
// TODO: Future enhancement - add section extraction
|
|
74
|
-
// Allow requesting specific sections of documentation:
|
|
75
|
-
// - Overview
|
|
76
|
-
// - Examples
|
|
77
|
-
// - API/Props
|
|
78
|
-
// - Accessibility
|
|
79
|
-
// - Test Hooks
|
|
80
|
-
//
|
|
81
|
-
// Example:
|
|
82
|
-
// export function getComponentSection(componentName: string, section: string): CallToolResult
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|