diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 0fa335797..f5f41af95 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -277,6 +277,9 @@ importers:
'@modelcontextprotocol/sdk':
specifier: 1.29.0
version: 1.29.0(zod@4.4.3)
+ '@nvidia-elements/code':
+ specifier: workspace:*
+ version: link:../code
'@nvidia-elements/lint':
specifier: workspace:^
version: link:../lint
@@ -22564,7 +22567,7 @@ snapshots:
magicast@0.5.2:
dependencies:
- '@babel/parser': 7.29.2
+ '@babel/parser': 7.29.3
'@babel/types': 7.29.0
source-map-js: 1.2.1
diff --git a/projects/cli/package.json b/projects/cli/package.json
index 6ef2888a7..c05da053f 100644
--- a/projects/cli/package.json
+++ b/projects/cli/package.json
@@ -34,7 +34,7 @@
"dist/**/*.js"
],
"scripts": {
- "dev": "pnpm run nve:install && pnpm dlx @modelcontextprotocol/inspector@0.21.2 node ./dist/index.js mcp",
+ "dev": "pnpm run nve:install && npx @modelcontextprotocol/inspector@0.21.2 node ./dist/index.js mcp",
"ci": "wireit",
"build": "wireit",
"lint": "wireit",
@@ -47,22 +47,23 @@
"nve:uninstall:node": "pnpm uninstall --global @nvidia-elements/cli"
},
"dependencies": {
- "@nvidia-elements/lint": "workspace:^",
"@inquirer/prompts": "8.4.2",
"@modelcontextprotocol/sdk": "1.29.0",
+ "@nvidia-elements/code": "workspace:*",
+ "@nvidia-elements/lint": "workspace:^",
"adm-zip": "0.5.17",
"archiver": "7.0.1",
"marked": "18.0.3",
"marked-terminal": "7.3.0",
+ "open": "11.0.0",
"ora": "9.4.0",
"publint": "catalog:",
"yargs": "18.0.0",
- "open": "11.0.0",
"zod": "catalog:"
},
"devDependencies": {
- "@internals/tools": "workspace:*",
"@internals/eslint": "workspace:*",
+ "@internals/tools": "workspace:*",
"@internals/vite": "workspace:*",
"@vitest/coverage-istanbul": "catalog:",
"@types/node": "catalog:",
@@ -117,6 +118,13 @@
"command": "NODE_ENV=production vite build",
"files": [
"../internals/tools/dist/**/*.js",
+ "../core/dist/bundles/index.js",
+ "../code/dist/bundles/index.js",
+ "../themes/dist/index.css",
+ "../themes/dist/dark.css",
+ "../themes/dist/high-contrast.css",
+ "../styles/dist/typography.css",
+ "../styles/dist/layout.css",
"src/**",
"!src/**/*.test.ts",
"package.json",
@@ -135,6 +143,22 @@
{
"script": "../internals/tools:build",
"cascade": false
+ },
+ {
+ "script": "../core:build",
+ "cascade": false
+ },
+ {
+ "script": "../code:build",
+ "cascade": false
+ },
+ {
+ "script": "../themes:build",
+ "cascade": false
+ },
+ {
+ "script": "../styles:build",
+ "cascade": false
}
]
},
diff --git a/projects/cli/src/index.test.ts b/projects/cli/src/index.test.ts
index 85efea96a..90f84335a 100644
--- a/projects/cli/src/index.test.ts
+++ b/projects/cli/src/index.test.ts
@@ -99,7 +99,8 @@ describe('index', () => {
input: ''
});
expect(result.status).toBe(1);
- expect(result.stderr + result.stdout).toContain('Invalid values');
+ expect(result.stdout).toBe('');
+ expect(result.stderr).toContain('Invalid values');
});
});
@@ -116,4 +117,38 @@ describe('index', () => {
expect(combined).toContain('nve-bar');
});
});
+
+ describe('tool errors', () => {
+ it('should exit with error when exact api lookup has no matches', () => {
+ const result = spawnSync('node', ['dist/index.js', 'api.get', 'nve-badges'], {
+ timeout: 10000,
+ encoding: 'utf-8',
+ input: ''
+ });
+
+ expect(result.status).toBe(1);
+ expect(result.stdout).toBe('');
+ expect(result.stderr).toContain('No components or APIs found matching');
+ expect(result.stderr).toContain('nve-badges');
+ });
+
+ it('should print structured error results when they are available', () => {
+ const result = spawnSync(
+ 'node',
+ ['dist/index.js', 'playground.create', 'hello'],
+ {
+ timeout: 10000,
+ encoding: 'utf-8',
+ input: '',
+ env: { ...process.env, CI: 'true', ELEMENTS_PLAYGROUND_BASE_URL: 'https://playground.example' }
+ }
+ );
+ const lintMessages = JSON.parse(result.stderr) as { message: string }[];
+
+ expect(result.status).toBe(1);
+ expect(result.stdout).toBe('');
+ expect(Array.isArray(lintMessages)).toBe(true);
+ expect(lintMessages[0]?.message).toContain('Unexpected use of restricted attribute "nve-layout"');
+ });
+ });
});
diff --git a/projects/cli/src/index.ts b/projects/cli/src/index.ts
index 8a7268daa..84bdfdca4 100755
--- a/projects/cli/src/index.ts
+++ b/projects/cli/src/index.ts
@@ -32,7 +32,7 @@ const yargsInstance = yargs(hideBin(process.argv))
}
if (message !== null) {
- console.log(colors.error(message));
+ console.error(colors.error(message));
}
process.exit(1);
});
@@ -45,6 +45,11 @@ yargsInstance.middleware(argv => {
}
});
+async function exitWithToolError(result: unknown, message: string | undefined): Promise {
+ console.error(result === undefined ? colors.error(message ?? 'unknown error') : await renderResult(result));
+ process.exit(1);
+}
+
yargsInstance.command(
'$0',
'About and help',
@@ -57,8 +62,7 @@ yargsInstance.command(
await renderResult(result);
process.exit(0);
} else {
- console.log(colors.error(message ?? 'unknown error'));
- process.exit(1);
+ await exitWithToolError(result, message);
}
} else {
const greeting = colors.complete(`\x1b[?7l\n${JSON.parse(banner)}\n\n`);
@@ -102,7 +106,6 @@ tools
optionalArgs.forEach(key => builder.option(key, argOptions(properties[key]!)));
},
// main handler for the command
- // eslint-disable-next-line max-statements
async args => {
const start = performance.now();
const { result, status, message } = await runAsyncTool(args, tool);
@@ -121,8 +124,7 @@ tools
await notifyIfUpdateAvailable(BUILD_SHA);
process.exit(0);
} else {
- console.log(colors.error(message ?? 'unknown error'));
- process.exit(1);
+ await exitWithToolError(result, message);
}
},
// middleware to get interactive arguments when missing
diff --git a/projects/cli/src/mcp/mcp.test.ts b/projects/cli/src/mcp/mcp.test.ts
index 57335e334..ad9f6cb42 100644
--- a/projects/cli/src/mcp/mcp.test.ts
+++ b/projects/cli/src/mcp/mcp.test.ts
@@ -3,70 +3,93 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
-const { mockRegisterTool, mockRegisterPrompt, mockConnect, mcpTool, cliTool, allTool, mockPrompt, mockZodSchema } =
- vi.hoisted(() => {
- const zodSchema = { shape: {}, optional: () => zodSchema };
-
- const createMockTool = (overrides: Record) => {
- const fn = vi.fn().mockResolvedValue({ status: 'complete', result: 'test' });
- Object.assign(fn, {
- metadata: {
- support: 3,
- summary: 'Test',
- description: 'Test description',
- title: 'Test',
- toolName: 'test_tool',
- command: 'test.tool',
- inputSchema: { type: 'object', properties: { name: { type: 'string' } } },
- ...overrides
- }
- });
- return fn;
- };
+const {
+ mockRegisterTool,
+ mockRegisterPrompt,
+ mockRegisterResource,
+ mockRegisterCapabilities,
+ mockConnect,
+ mcpTool,
+ cliTool,
+ allTool,
+ uiTool,
+ mockPrompt,
+ mockZodSchema
+} = vi.hoisted(() => {
+ const zodSchema = { shape: {}, optional: () => zodSchema };
- return {
- mockRegisterTool: vi.fn(),
- mockRegisterPrompt: vi.fn(),
- mockConnect: vi.fn().mockResolvedValue(undefined),
- mockZodSchema: zodSchema,
- mcpTool: createMockTool({
- support: 1,
- toolName: 'mcp_tool',
- command: 'mcp.tool',
- title: 'MCP Tool',
- summary: 'MCP only',
- inputSchema: undefined
- }),
- cliTool: createMockTool({
- support: 2,
- toolName: 'cli_tool',
- command: 'cli_tool',
- title: 'CLI Tool',
- summary: 'CLI only'
- }),
- allTool: createMockTool({
+ const createMockTool = (overrides: Record) => {
+ const fn = vi.fn().mockResolvedValue({ status: 'complete', result: 'test' });
+ Object.assign(fn, {
+ metadata: {
support: 3,
- toolName: 'all_tool',
- command: 'all.tool',
- title: 'All Tool',
- summary: 'All support',
- outputSchema: { type: 'string' }
- }),
- mockPrompt: {
- name: 'test-prompt',
- title: 'Test Prompt',
- description: 'A test prompt',
- argsSchema: { type: 'object', properties: {} },
- handler: vi.fn().mockResolvedValue({ messages: [] })
+ summary: 'Test',
+ description: 'Test description',
+ title: 'Test',
+ toolName: 'test_tool',
+ command: 'test.tool',
+ inputSchema: { type: 'object', properties: { name: { type: 'string' } } },
+ ...overrides
}
- };
- });
+ });
+ return fn;
+ };
+
+ return {
+ mockRegisterTool: vi.fn(),
+ mockRegisterPrompt: vi.fn(),
+ mockRegisterResource: vi.fn(),
+ mockRegisterCapabilities: vi.fn(),
+ mockConnect: vi.fn().mockResolvedValue(undefined),
+ mockZodSchema: zodSchema,
+ mcpTool: createMockTool({
+ support: 1,
+ toolName: 'mcp_tool',
+ command: 'mcp.tool',
+ title: 'MCP Tool',
+ summary: 'MCP only',
+ inputSchema: undefined
+ }),
+ cliTool: createMockTool({
+ support: 2,
+ toolName: 'cli_tool',
+ command: 'cli_tool',
+ title: 'CLI Tool',
+ summary: 'CLI only'
+ }),
+ allTool: createMockTool({
+ support: 3,
+ toolName: 'all_tool',
+ command: 'all.tool',
+ title: 'All Tool',
+ summary: 'All support',
+ outputSchema: { type: 'string' }
+ }),
+ uiTool: createMockTool({
+ support: 1,
+ toolName: 'mcp_ui_tool',
+ command: 'mcp.ui.tool',
+ title: 'MCP UI Tool',
+ summary: 'MCP UI-enabled tool',
+ app: { resourceUri: 'ui://elements/example-preview' }
+ }),
+ mockPrompt: {
+ name: 'test-prompt',
+ title: 'Test Prompt',
+ description: 'A test prompt',
+ argsSchema: { type: 'object', properties: {} },
+ handler: vi.fn().mockResolvedValue({ messages: [] })
+ }
+ };
+});
vi.mock('@modelcontextprotocol/sdk/server/mcp.js', () => ({
McpServer: vi.fn(function () {
return {
registerTool: mockRegisterTool,
registerPrompt: mockRegisterPrompt,
+ registerResource: mockRegisterResource,
+ server: { registerCapabilities: mockRegisterCapabilities },
connect: mockConnect
};
})
@@ -77,7 +100,7 @@ vi.mock('@modelcontextprotocol/sdk/server/stdio.js', () => ({
}));
vi.mock('@internals/tools', () => ({
- tools: [mcpTool, cliTool, allTool],
+ tools: [mcpTool, cliTool, allTool, uiTool],
prompts: [mockPrompt],
jsonSchemaToZod: vi.fn(() => mockZodSchema),
ToolSupport: { None: 0, MCP: 1, CLI: 2, All: 3 }
@@ -103,9 +126,10 @@ describe('MCP server', () => {
it('should only register tools with MCP support', async () => {
const { startMcpServer } = await import('./mcp.js');
await startMcpServer();
- expect(mockRegisterTool).toHaveBeenCalledTimes(2);
+ expect(mockRegisterTool).toHaveBeenCalledTimes(3);
expect(mockRegisterTool).toHaveBeenCalledWith('mcp_tool', expect.any(Object), expect.any(Function));
expect(mockRegisterTool).toHaveBeenCalledWith('all_tool', expect.any(Object), expect.any(Function));
+ expect(mockRegisterTool).toHaveBeenCalledWith('mcp_ui_tool', expect.any(Object), expect.any(Function));
});
it('should not register CLI-only tools', async () => {
@@ -195,6 +219,147 @@ describe('MCP server', () => {
expect(result.content[0].text).toBe(JSON.stringify(objResult));
});
+ it('should advertise the io.modelcontextprotocol/ui extension capability', async () => {
+ const { startMcpServer } = await import('./mcp.js');
+ await startMcpServer();
+ expect(mockRegisterCapabilities).toHaveBeenCalledWith({
+ extensions: { 'io.modelcontextprotocol/ui': { mimeTypes: ['text/html;profile=mcp-app'] } }
+ });
+ });
+
+ it('should register the MCP UI resources', async () => {
+ const { startMcpServer } = await import('./mcp.js');
+ await startMcpServer();
+ [
+ {
+ name: 'nve-mcp-examples-render',
+ uri: 'ui://elements/example-preview',
+ title: 'Elements Example Preview'
+ },
+ {
+ name: 'nve-mcp-api-icons-list',
+ uri: 'ui://elements/icons-list',
+ title: 'Elements Icons List'
+ },
+ {
+ name: 'nve-mcp-api-tokens-list',
+ uri: 'ui://elements/tokens-list',
+ title: 'Elements Token Explorer'
+ }
+ ].forEach(({ name, uri, title }) => {
+ const resourceCall = mockRegisterResource.mock.calls.find(call => call[0] === name);
+ expect(resourceCall).toBeDefined();
+ const [, registeredUri, config, handler] = resourceCall!;
+ expect(registeredUri).toBe(uri);
+ expect(config).toMatchObject({ mimeType: 'text/html;profile=mcp-app' });
+ const result = handler();
+ expect(result.contents[0]).toMatchObject({
+ uri,
+ mimeType: 'text/html;profile=mcp-app'
+ });
+ expect(result.contents[0].text).toContain('');
+ expect(result.contents[0].text).toContain(`${title}`);
+ });
+ });
+
+ it('should restrict MCP UI messages to the expected parent origin', async () => {
+ const { startMcpServer } = await import('./mcp.js');
+ await startMcpServer();
+ const resourceCall = mockRegisterResource.mock.calls.find(call => call[0] === 'nve-mcp-examples-render');
+ const [, , , handler] = resourceCall!;
+ const result = handler();
+ const html = result.contents[0].text;
+
+ expect(html).toContain('window.parent.postMessage(msg, expectedOrigin)');
+ expect(html).toContain('#getExpectedParentOrigin()');
+ expect(html).toContain('#isAllowedParentOrigin(origin)');
+ expect(html).toContain("origin !== 'null'");
+ expect(html).not.toContain("window.parent.postMessage(msg, '*')");
+ });
+
+ it('should keep the example preview element decoupled from the MCP UI client', async () => {
+ const { startMcpServer } = await import('./mcp.js');
+ await startMcpServer();
+ const resourceCall = mockRegisterResource.mock.calls.find(call => call[0] === 'nve-mcp-examples-render');
+ const [, , , handler] = resourceCall!;
+ const result = handler();
+ const html = result.contents[0].text;
+ const elementScript = html.slice(
+ html.indexOf('class ElementsExamplePreview'),
+ html.indexOf("customElements.define('nve-mcp-examples-render'")
+ );
+ const clientScript = html.slice(html.indexOf('const client = new Client'));
+
+ expect(elementScript).not.toContain('callServerTool');
+ expect(elementScript).not.toContain('handleToolInput');
+ expect(elementScript).not.toContain('handleToolResult');
+ expect(elementScript).not.toContain('structuredContent');
+ expect(elementScript).not.toMatch(/this\.client\s*=/);
+ expect(clientScript).toContain('client.callServerTool');
+ expect(clientScript).toContain("name: 'examples_get'");
+ expect(clientScript).toContain('lintMessages');
+ expect(clientScript).not.toContain('preview.template = pendingTemplate');
+ });
+
+ it('should keep the icons list element decoupled from the MCP UI client', async () => {
+ const { startMcpServer } = await import('./mcp.js');
+ await startMcpServer();
+ const resourceCall = mockRegisterResource.mock.calls.find(call => call[0] === 'nve-mcp-api-icons-list');
+ const [, , , handler] = resourceCall!;
+ const result = handler();
+ const html = result.contents[0].text;
+ const elementScript = html.slice(
+ html.indexOf('class ElementsIconsList'),
+ html.indexOf("customElements.define('nve-mcp-api-icons-list'")
+ );
+ const clientScript = html.slice(html.indexOf('const client = new Client'));
+
+ expect(elementScript).toContain("new CustomEvent('icons-request'");
+ expect(elementScript).not.toContain('callServerTool');
+ expect(elementScript).not.toContain('handleToolResult');
+ expect(elementScript).not.toContain('structuredContent');
+ expect(elementScript).not.toMatch(/this\.client\s*=/);
+ expect(clientScript).toContain('client.callServerTool');
+ expect(clientScript).toContain("iconList.addEventListener('icons-request'");
+ });
+
+ it('should keep the tokens list element decoupled from the MCP UI client', async () => {
+ const { startMcpServer } = await import('./mcp.js');
+ await startMcpServer();
+ const resourceCall = mockRegisterResource.mock.calls.find(call => call[0] === 'nve-mcp-api-tokens-list');
+ const [, , , handler] = resourceCall!;
+ const result = handler();
+ const html = result.contents[0].text;
+ const elementScript = html.slice(
+ html.indexOf('class ElementsTokensList'),
+ html.indexOf("customElements.define('nve-mcp-api-tokens-list'")
+ );
+ const clientScript = html.slice(html.indexOf('const client = new Client'));
+
+ expect(elementScript).toContain("new CustomEvent('tokens-request'");
+ expect(elementScript).not.toContain('callServerTool');
+ expect(elementScript).not.toContain('handleToolInput');
+ expect(elementScript).not.toContain('handleToolResult');
+ expect(elementScript).not.toContain('structuredContent');
+ expect(elementScript).not.toMatch(/this\.client\s*=/);
+ expect(clientScript).toContain('client.callServerTool');
+ expect(clientScript).toContain("tokenList.addEventListener('tokens-request'");
+ });
+
+ it('should attach _meta.ui to tools that declare MCP UI metadata', async () => {
+ const { startMcpServer } = await import('./mcp.js');
+ await startMcpServer();
+ const uiToolCall = mockRegisterTool.mock.calls.find(call => call[0] === 'mcp_ui_tool');
+ expect(uiToolCall![1]._meta).toEqual({ ui: { resourceUri: 'ui://elements/example-preview' } });
+ });
+
+ it('should leave _meta undefined for tools without MCP UI metadata', async () => {
+ const { startMcpServer } = await import('./mcp.js');
+ await startMcpServer();
+ const mcpToolCall = mockRegisterTool.mock.calls.find(call => call[0] === 'mcp_tool');
+ expect(mcpToolCall![1]._meta).toBeUndefined();
+ });
+
it('should exit on connection error', async () => {
mockConnect.mockRejectedValueOnce(new Error('Connection failed'));
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => undefined as never);
diff --git a/projects/cli/src/mcp/mcp.ts b/projects/cli/src/mcp/mcp.ts
index f61833d34..0b21b2b9a 100644
--- a/projects/cli/src/mcp/mcp.ts
+++ b/projects/cli/src/mcp/mcp.ts
@@ -7,20 +7,49 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import { tools, prompts, jsonSchemaToZod, ToolSupport } from '@internals/tools';
import z, { type ZodObject } from 'zod';
import { type ToolAnnotations } from '@modelcontextprotocol/sdk/types.js';
+import { MCP_UI_MIME_TYPE, uiResources } from './ui/index.js';
const VERSION = '0.0.0';
-// eslint-disable-next-line max-lines-per-function
-export async function startMcpServer() {
- process.env.ELEMENTS_ENV = 'mcp';
+function registerCapabilities(server: McpServer): void {
+ server.server.registerCapabilities({
+ extensions: { 'io.modelcontextprotocol/ui': { mimeTypes: [MCP_UI_MIME_TYPE] } }
+ });
+}
- const server = new McpServer({
- name: 'nve-mcp',
- version: VERSION,
- description:
- 'NVIDIA Elements UI Design System (nve-*), custom element schemas, APIs and examples. Use the "elements" skill for more guidance if available.'
+function registerResources(server: McpServer): void {
+ uiResources.forEach(resource => {
+ server.registerResource(
+ resource.name,
+ resource.resourceUri,
+ { mimeType: resource.mimeType, description: resource.description },
+ () => ({
+ contents: [{ uri: resource.resourceUri, mimeType: resource.mimeType, text: resource.getHtml() }]
+ })
+ );
});
+}
+function attachProgress(
+ params: Record,
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ extra: any
+): void {
+ const progressToken = extra?._meta?.progressToken;
+ if (progressToken === undefined) return;
+ let progressCount = 0;
+ params.onProgress = (message: string) => {
+ progressCount++;
+ extra
+ .sendNotification({
+ method: 'notifications/progress',
+ params: { progressToken, progress: progressCount, message }
+ })
+ .catch(() => undefined);
+ };
+}
+
+function registerTools(server: McpServer): void {
tools
.filter(tool => tool.metadata.support & ToolSupport.MCP)
.forEach(tool => {
@@ -29,40 +58,27 @@ export async function startMcpServer() {
? (jsonSchemaToZod(tool.metadata.inputSchema) as ZodObject>).shape
: {};
const resultSchema = tool.metadata.outputSchema ? jsonSchemaToZod(tool.metadata.outputSchema) : z.any();
- const outputSchema = {
- status: z.enum(['complete', 'error']).optional(),
- message: z.string().optional(),
- result: resultSchema.optional()
- };
-
const config = {
title,
inputSchema,
- outputSchema,
+ outputSchema: {
+ status: z.enum(['complete', 'error']).optional(),
+ message: z.string().optional(),
+ result: resultSchema.optional()
+ },
description: description ? description : summary,
annotations: {
title,
- readOnlyHint: true, // If true, the tool does not change its environment
- idempotentHint: true, // If true, repeated calls with same args have no extra effect
- destructiveHint: false, // If true, the tool may perform destructive/irreversible updates
- openWorldHint: false, // If true, tool interacts with external entities
+ readOnlyHint: true,
+ idempotentHint: true,
+ destructiveHint: false,
+ openWorldHint: false,
...tool.metadata.annotations
- } as ToolAnnotations
+ } as ToolAnnotations,
+ _meta: !tool.metadata.app ? undefined : { ui: { resourceUri: tool.metadata.app.resourceUri } }
};
server.registerTool(toolName, config, async (params, extra) => {
- const progressToken = extra?._meta?.progressToken;
- let progressCount = 0;
- if (progressToken !== undefined) {
- (params as Record).onProgress = (message: string) => {
- progressCount++;
- extra
- .sendNotification({
- method: 'notifications/progress',
- params: { progressToken, progress: progressCount, message }
- })
- .catch(() => {});
- };
- }
+ attachProgress(params as Record, extra);
const structuredContent = (await tool(params)) as unknown as { [x: string]: unknown };
// https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1624
const text =
@@ -72,12 +88,30 @@ export async function startMcpServer() {
return { structuredContent, content: [{ type: 'text', text }] };
});
});
+}
+function registerPrompts(server: McpServer): void {
prompts.forEach(prompt => {
const argsSchema = (jsonSchemaToZod(prompt.argsSchema ?? {}) as ZodObject>).shape;
const config = { title: prompt.title, description: prompt.description, argsSchema };
server.registerPrompt(prompt.name, config, async params => prompt.handler(params));
});
+}
+
+export async function startMcpServer() {
+ process.env.ELEMENTS_ENV = 'mcp';
+
+ const server = new McpServer({
+ name: 'nve-mcp',
+ version: VERSION,
+ description:
+ 'NVIDIA Elements UI Design System (nve-*), custom element schemas, APIs and examples. Use the "elements" skill for more guidance if available.'
+ });
+
+ registerCapabilities(server);
+ registerResources(server);
+ registerTools(server);
+ registerPrompts(server);
try {
await server.connect(new StdioServerTransport());
diff --git a/projects/cli/src/mcp/ui/api-icons-list.js b/projects/cli/src/mcp/ui/api-icons-list.js
new file mode 100644
index 000000000..c61264158
--- /dev/null
+++ b/projects/cli/src/mcp/ui/api-icons-list.js
@@ -0,0 +1,311 @@
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+const iconsListStyle = /* css */ `
+:host {
+ display: block;
+}
+
+.icons-shell {
+ display: flex;
+ flex-direction: column;
+ gap: var(--nve-ref-space-sm);
+}
+
+.icons-count {
+ color: var(--nve-sys-text-muted-color);
+ font-size: var(--nve-ref-font-size-100);
+ font-weight: var(--nve-ref-font-weight-bold);
+ line-height: var(--nve-ref-line-height-sm);
+}
+
+.icons-grid,
+.icons-empty,
+.icons-error {
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--nve-ref-space-xs);
+ max-height: 480px;
+ overflow-y: auto;
+ scrollbar-color: var(--nve-sys-scrollbar-thumb-color) var(--nve-sys-scrollbar-track-color);
+ scrollbar-width: var(--nve-sys-scrollbar-width);
+}
+
+.icons-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
+ gap: var(--nve-ref-space-xs);
+
+ nve-button {
+ --height: 80px;
+ --border-radius: var(--nve-ref-border-radius-sm);
+ --font-size: var(--nve-ref-font-size-50);
+ }
+
+ .icon-button-content {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: var(--nve-ref-space-xs);
+ }
+}
+`;
+
+const iconsListStyleSheet = new CSSStyleSheet();
+iconsListStyleSheet.replaceSync(iconsListStyle);
+
+class ElementsIconsList extends HTMLElement {
+ constructor() {
+ super();
+ this.renderRoot = this.attachShadow({ mode: 'open' });
+ this.renderRoot.adoptedStyleSheets = [iconsListStyleSheet];
+ this.state = { icons: [], errorMessage: '', loaded: false, loading: false, query: '' };
+ this.grid = null;
+ this.count = null;
+ this.copiedToast = null;
+ }
+
+ get icons() {
+ return this.state.icons;
+ }
+
+ set icons(value) {
+ const icons = Array.isArray(value) ? value.filter(name => typeof name === 'string' && name.length > 0) : [];
+ this.#setIcons(icons);
+ }
+
+ get errorMessage() {
+ return this.state.errorMessage;
+ }
+
+ set errorMessage(value) {
+ this.state.errorMessage = typeof value === 'string' ? value : '';
+ this.state.loaded = false;
+ this.state.loading = false;
+ this.#renderCurrentState();
+ }
+
+ connectedCallback() {
+ this.#renderShell();
+ this.#requestIcons();
+ }
+
+ #iconSource(name) {
+ return '';
+ }
+
+ #renderShell() {
+ const root = document.createElement('main');
+ root.className = 'icons-shell';
+ this.count = document.createElement('span');
+ this.count.className = 'icons-count';
+
+ const searchControl = document.createElement('nve-search');
+ searchControl.setAttribute('rounded', '');
+ const search = document.createElement('input');
+ search.type = 'search';
+ search.placeholder = 'Search icons';
+ search.setAttribute('aria-label', 'Search icons');
+ search.autocomplete = 'off';
+ search.addEventListener('input', () => {
+ this.state.query = search.value.trim().toLowerCase();
+ if (this.state.loaded) {
+ this.#renderIcons();
+ }
+ });
+ searchControl.append(search);
+
+ this.grid = document.createElement('section');
+
+ this.copiedToast = document.createElement('nve-toast');
+ this.copiedToast.setAttribute('hidden', '');
+ this.copiedToast.setAttribute('status', 'success');
+ this.copiedToast.setAttribute('position', 'top');
+ this.copiedToast.setAttribute('close-timeout', '1500');
+ this.copiedToast.textContent = 'Copied';
+
+ root.append(this.count, searchControl, this.grid, this.copiedToast);
+ this.renderRoot.replaceChildren(root);
+ this.#renderCurrentState();
+ }
+
+ #renderCurrentState() {
+ if (!this.count || !this.grid) return;
+ if (this.state.errorMessage) {
+ this.#renderError(this.state.errorMessage);
+ return;
+ }
+ if (this.state.loaded) {
+ this.#renderIcons();
+ return;
+ }
+ this.#renderLoading();
+ }
+
+ #renderLoading() {
+ this.count.textContent = 'Loading';
+ this.grid.className = 'icons-empty';
+ this.grid.textContent = 'Loading icons';
+ }
+
+ #renderError(message) {
+ this.count.textContent = 'Unavailable';
+ this.grid.className = 'icons-error';
+ this.grid.textContent = message;
+ }
+
+ #renderIcons() {
+ const filtered = this.state.query
+ ? this.state.icons.filter(name => name.includes(this.state.query))
+ : this.state.icons;
+ this.count.textContent = filtered.length + ' of ' + this.state.icons.length;
+ this.grid.className = filtered.length ? 'icons-grid' : 'icons-empty';
+ this.grid.replaceChildren();
+ if (!filtered.length) {
+ this.grid.textContent = 'No icons found';
+ return;
+ }
+ filtered.forEach(name => this.grid.append(this.#createIconCard(name)));
+ }
+
+ #createIconCard(name) {
+ const copyButton = document.createElement('nve-button');
+ copyButton.setAttribute('aria-label', 'Copy ' + name + ' source');
+ copyButton.title = 'Copy source';
+
+ const copyButtonContent = document.createElement('div');
+ copyButtonContent.className = 'icon-button-content';
+
+ const icon = document.createElement('nve-icon');
+ icon.setAttribute('name', name);
+ icon.setAttribute('aria-hidden', 'true');
+ icon.size = 'lg';
+
+ const iconName = document.createElement('span');
+ iconName.className = 'icon-name';
+ iconName.textContent = name;
+
+ copyButtonContent.append(icon, iconName);
+ copyButton.append(copyButtonContent);
+ copyButton.addEventListener('click', () => {
+ void this.#copySource(this.#iconSource(name), copyButton);
+ });
+
+ return copyButton;
+ }
+
+ async #copySource(source, button) {
+ try {
+ if (navigator.clipboard && navigator.clipboard.writeText) {
+ await navigator.clipboard.writeText(source);
+ } else {
+ this.#copyWithTextArea(source);
+ }
+ } catch (_err) {
+ this.#copyWithTextArea(source);
+ }
+ button.dataset.copied = 'true';
+ this.#showCopiedToast(button);
+ window.setTimeout(() => {
+ button.dataset.copied = 'false';
+ }, 1200);
+ }
+
+ #showCopiedToast(button) {
+ if (!this.copiedToast) return;
+ if (this.copiedToast.matches(':popover-open')) {
+ this.copiedToast.hidePopover();
+ }
+ this.copiedToast.showPopover({ source: button });
+ }
+
+ #copyWithTextArea(source) {
+ const textarea = document.createElement('textarea');
+ textarea.value = source;
+ textarea.setAttribute('readonly', '');
+ textarea.style.position = 'fixed';
+ textarea.style.opacity = '0';
+ document.body.append(textarea);
+ textarea.select();
+ document.execCommand('copy');
+ textarea.remove();
+ }
+
+ #setIcons(icons) {
+ this.state.icons = icons;
+ this.state.errorMessage = '';
+ this.state.loaded = true;
+ this.state.loading = false;
+ this.#renderCurrentState();
+ }
+
+ #requestIcons() {
+ if (this.state.loaded || this.state.loading) return;
+ this.state.loading = true;
+ this.dispatchEvent(new CustomEvent('icons-request', { bubbles: true, composed: true }));
+ }
+}
+
+customElements.define('nve-mcp-api-icons-list', ElementsIconsList);
+
+const client = new Client({ name: 'Elements Icons List', version: '1.0.0' });
+
+const iconList = document.createElement('nve-mcp-api-icons-list');
+
+function getStructuredResult(payload) {
+ if (!payload) return undefined;
+ if (payload.structuredContent && payload.structuredContent.result !== undefined)
+ return payload.structuredContent.result;
+ if (payload.result && payload.result.structuredContent) return payload.result.structuredContent.result;
+ if (payload.result !== undefined) return payload.result;
+ return undefined;
+}
+
+function getIconNames(payload) {
+ const result = getStructuredResult(payload);
+ return Array.isArray(result) ? result.filter(name => typeof name === 'string' && name.length > 0) : [];
+}
+
+let iconsLoaded = false;
+let iconsLoading = false;
+
+function setIconsFromPayload(payload) {
+ const icons = getIconNames(payload);
+ if (!icons.length) return false;
+ iconsLoaded = true;
+ iconsLoading = false;
+ iconList.icons = icons;
+ return true;
+}
+
+async function loadIcons() {
+ if (iconsLoaded || iconsLoading) return;
+ iconsLoading = true;
+ try {
+ const res = await client.callServerTool({
+ name: 'api_icons_list',
+ arguments: { format: 'json' }
+ });
+ if (!setIconsFromPayload(res)) {
+ iconsLoading = false;
+ iconList.errorMessage = 'Failed to load icons: no icons returned';
+ }
+ } catch (err) {
+ iconsLoading = false;
+ iconList.errorMessage = 'Failed to load icons: ' + (err && err.message ? err.message : 'unknown error');
+ }
+}
+
+iconList.addEventListener('icons-request', () => {
+ void loadIcons();
+});
+
+client.ontoolresult = params => {
+ if (!setIconsFromPayload(params)) {
+ void loadIcons();
+ }
+};
+
+client.connect();
+document.body.replaceChildren(iconList);
diff --git a/projects/cli/src/mcp/ui/api-tokens-list.js b/projects/cli/src/mcp/ui/api-tokens-list.js
new file mode 100644
index 000000000..ea55bfb56
--- /dev/null
+++ b/projects/cli/src/mcp/ui/api-tokens-list.js
@@ -0,0 +1,712 @@
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+const tokensListStyle = /* css */ `
+ :host {
+ display: block;
+ }
+
+ .tokens-shell {
+ display: grid;
+ gap: var(--nve-ref-space-sm);
+ }
+
+ .tokens-toolbar {
+ align-items: center;
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--nve-ref-space-sm);
+ }
+
+ .tokens-count,
+ .token-section-count {
+ color: var(--nve-sys-text-muted-color);
+ font-size: var(--nve-ref-font-size-100);
+ font-weight: var(--nve-ref-font-weight-bold);
+ line-height: var(--nve-ref-line-height-sm);
+ }
+
+ .tokens-search {
+ flex: 1 1 240px;
+ min-width: 200px;
+ }
+
+ .theme-toggle {
+ display: flex;
+ flex: 0 0 auto;
+ flex-wrap: wrap;
+ gap: var(--nve-ref-space-xs);
+ }
+
+ .tokens-groups {
+ display: grid;
+ gap: var(--nve-ref-space-lg);
+ margin-top: var(--nve-ref-space-md);
+ max-height: 480px;
+ overflow-y: auto;
+ padding-right: var(--nve-ref-size-100);
+ scrollbar-color: var(--nve-sys-scrollbar-thumb-color) var(--nve-sys-scrollbar-track-color);
+ scrollbar-width: var(--nve-sys-scrollbar-width);
+ }
+
+ .token-title-row {
+ align-items: center;
+ display: flex;
+ gap: var(--nve-ref-space-xs);
+ }
+
+ .token-heading {
+ color: var(--nve-sys-text-emphasis-color);
+ font-size: var(--nve-ref-font-size-400);
+ font-weight: var(--nve-ref-font-weight-bold);
+ line-height: var(--nve-ref-line-height-sm);
+ margin: 0;
+ }
+
+ .token-table {
+ --scroll-height: none;
+ width: 100%;
+ }
+
+ .token-preview-cell {
+ --justify-content: center;
+ }
+
+ .token-description {
+ color: var(--nve-sys-text-muted-color);
+ display: block;
+ font-size: var(--nve-ref-font-size-100);
+ line-height: var(--nve-ref-line-height-sm);
+ min-width: 0;
+ overflow-wrap: anywhere;
+ }
+
+ .token-preview {
+ border: var(--nve-ref-border-width-sm) solid var(--nve-ref-border-color-muted);
+ border-radius: var(--nve-ref-border-radius-sm);
+ display: block;
+ min-height: var(--nve-ref-size-800);
+ overflow: hidden;
+ width: min(100%, 96px);
+ }
+
+ .color-preview {
+ background-image:
+ linear-gradient(45deg, color-mix(in oklab, CanvasText 12%, transparent) 25%, transparent 25%),
+ linear-gradient(-45deg, color-mix(in oklab, CanvasText 12%, transparent) 25%, transparent 25%),
+ linear-gradient(45deg, transparent 75%, color-mix(in oklab, CanvasText 12%, transparent) 75%),
+ linear-gradient(-45deg, transparent 75%, color-mix(in oklab, CanvasText 12%, transparent) 75%);
+ background-position: 0 0, 0 6px, 6px -6px, -6px 0;
+ background-size: 12px 12px;
+ }
+
+ .color-swatch {
+ display: block;
+ min-height: var(--nve-ref-size-800);
+ }
+
+ .ruler-preview {
+ align-items: center;
+ background: var(--nve-sys-layer-container-accent-background);
+ display: flex;
+ padding: var(--nve-ref-size-100);
+ }
+
+ .ruler-fill {
+ background: var(--nve-sys-support-color);
+ border-radius: var(--nve-ref-border-radius-xs);
+ display: block;
+ height: var(--nve-ref-size-200);
+ }
+
+ .type-preview {
+ align-items: center;
+ background: var(--nve-sys-layer-container-accent-background);
+ color: var(--nve-sys-text-color);
+ display: flex;
+ min-height: var(--nve-ref-size-1000);
+ padding: var(--nve-ref-size-100) var(--nve-ref-size-200);
+ }
+
+ .radius-preview {
+ align-items: center;
+ background: var(--nve-sys-layer-container-accent-background);
+ display: flex;
+ justify-content: center;
+ min-height: var(--nve-ref-size-1000);
+ padding: var(--nve-ref-size-100);
+ }
+
+ .radius-shape {
+ background: var(--nve-sys-layer-container-accent-background);
+ border: var(--nve-ref-border-width-md) solid var(--nve-ref-border-color-emphasis);
+ display: block;
+ height: var(--nve-ref-size-800);
+ width: 80px;
+ }
+
+ .shadow-preview {
+ align-items: center;
+ background: var(--nve-sys-layer-canvas-accent-background);
+ display: flex;
+ justify-content: center;
+ min-height: var(--nve-ref-size-1000);
+ padding: var(--nve-ref-size-100);
+ }
+
+ .shadow-shape {
+ background: var(--nve-sys-layer-container-background);
+ border: var(--nve-ref-border-width-sm) solid var(--nve-ref-border-color-muted);
+ border-radius: var(--nve-ref-border-radius-sm);
+ display: block;
+ height: var(--nve-ref-size-800);
+ width: 80px;
+ }
+
+ .token-empty,
+ .token-error {
+ color: var(--nve-sys-text-muted-color);
+ padding-block: var(--nve-ref-size-400);
+ }
+`;
+
+const tokensListStyleSheet = new CSSStyleSheet();
+tokensListStyleSheet.replaceSync(tokensListStyle);
+
+const colorValuePrefixes = ['#', 'rgb', 'hsl', 'oklch'];
+const colorValueNames = new Set([
+ 'canvas',
+ 'canvastext',
+ 'buttonface',
+ 'buttontext',
+ 'fieldtext',
+ 'graytext',
+ 'linktext'
+]);
+const tokenCategoryMatchers = [
+ { category: 'shadow', nameKeywords: ['shadow'] },
+ { category: 'radius', nameKeywords: ['radius', 'border-width'] },
+ { category: 'typography', nameKeywords: ['font'] },
+ { category: 'spacing', nameKeywords: ['space', 'size', 'height', 'width', 'gap', 'padding', 'offset', 'margin'] },
+ {
+ category: 'color',
+ nameKeywords: ['color', 'background', 'accent', 'status', 'support', 'visualization'],
+ valueChecks: [
+ value => colorValuePrefixes.some(prefix => value.startsWith(prefix)),
+ value => colorValueNames.has(value)
+ ]
+ }
+];
+
+class ElementsTokensList extends HTMLElement {
+ constructor() {
+ super();
+ this.renderRoot = this.attachShadow({ mode: 'open' });
+ this.renderRoot.adoptedStyleSheets = [tokensListStyleSheet];
+ this.state = { tokens: [], errorMessage: '', loaded: false, loading: false, query: '', theme: '' };
+ this.count = null;
+ this.groups = null;
+ this.searchInput = null;
+ this.copiedToast = null;
+ this.themeButtons = new Map();
+ }
+
+ connectedCallback() {
+ this.#renderShell();
+ this.#setTheme(this.#getPreferredTheme());
+ this.#requestTokens();
+ }
+
+ get tokens() {
+ return this.state.tokens;
+ }
+
+ set tokens(value) {
+ const tokens = Array.isArray(value)
+ ? value
+ .filter(token => token && typeof token.name === 'string' && typeof token.value === 'string')
+ .map(token => ({
+ name: token.name.startsWith('--') ? token.name : '--' + token.name,
+ value: token.value,
+ description: typeof token.description === 'string' ? token.description : ''
+ }))
+ : [];
+ this.#setTokens(tokens);
+ }
+
+ get errorMessage() {
+ return this.state.errorMessage;
+ }
+
+ set errorMessage(value) {
+ this.state.errorMessage = typeof value === 'string' ? value : '';
+ this.state.loaded = false;
+ this.state.loading = false;
+ this.#renderCurrentState();
+ }
+
+ get query() {
+ return this.state.query;
+ }
+
+ set query(value) {
+ this.#setQuery(value);
+ }
+
+ #tokenReference(name) {
+ return 'var(' + name + ')';
+ }
+
+ #getPreferredTheme() {
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
+ }
+
+ #setQuery(query, render = true) {
+ const value = typeof query === 'string' ? query.trim() : '';
+ this.state.query = value.toLowerCase();
+ if (this.searchInput && this.searchInput.value !== value) {
+ this.searchInput.value = value;
+ }
+ if (render && this.state.loaded) {
+ this.#renderTokens();
+ }
+ }
+
+ #classifyToken(token) {
+ const name = token.name.toLowerCase();
+ const value = token.value.toLowerCase();
+ const match = tokenCategoryMatchers.find(({ nameKeywords, valueChecks = [] }) => {
+ return nameKeywords.some(keyword => name.includes(keyword)) || valueChecks.some(check => check(value));
+ });
+
+ return match?.category ?? 'other';
+ }
+
+ #categoryLabel(category) {
+ switch (category) {
+ case 'color':
+ return 'Colors';
+ case 'spacing':
+ return 'Spacing and size';
+ case 'typography':
+ return 'Typography';
+ case 'radius':
+ return 'Radii and borders';
+ case 'shadow':
+ return 'Shadows';
+ case 'other':
+ return 'Other';
+ default:
+ return 'Tokens';
+ }
+ }
+
+ #renderShell() {
+ const root = document.createElement('main');
+ root.className = 'tokens-shell';
+
+ const toolbar = document.createElement('section');
+ toolbar.className = 'tokens-toolbar';
+
+ this.count = document.createElement('span');
+ this.count.className = 'tokens-count';
+
+ const searchControl = document.createElement('nve-search');
+ searchControl.className = 'tokens-search';
+ searchControl.setAttribute('rounded', '');
+ const search = document.createElement('input');
+ search.type = 'search';
+ search.placeholder = 'Search tokens';
+ search.setAttribute('aria-label', 'Search tokens');
+ search.autocomplete = 'off';
+ this.searchInput = search;
+ search.addEventListener('input', () => {
+ this.#setQuery(search.value);
+ });
+ searchControl.append(search);
+
+ const themeToggle = document.createElement('section');
+ themeToggle.className = 'theme-toggle';
+ themeToggle.setAttribute('role', 'group');
+ themeToggle.setAttribute('aria-label', 'Theme');
+ [
+ ['light', 'Light'],
+ ['dark', 'Dark'],
+ ['high-contrast', 'High contrast']
+ ].forEach(([theme, label]) => {
+ const button = document.createElement('nve-button');
+ button.setAttribute('type', 'button');
+ button.setAttribute('size', 'sm');
+ button.textContent = label;
+ button.addEventListener('click', () => this.#setTheme(theme));
+ this.themeButtons.set(theme, button);
+ themeToggle.append(button);
+ });
+
+ toolbar.append(this.count, searchControl, themeToggle);
+
+ this.groups = document.createElement('section');
+ this.groups.className = 'tokens-groups';
+
+ this.copiedToast = document.createElement('nve-toast');
+ this.copiedToast.setAttribute('hidden', '');
+ this.copiedToast.setAttribute('status', 'success');
+ this.copiedToast.setAttribute('position', 'top');
+ this.copiedToast.setAttribute('close-timeout', '1500');
+ this.copiedToast.textContent = 'Copied';
+
+ root.append(toolbar, this.groups, this.copiedToast);
+ this.renderRoot.replaceChildren(root);
+ this.#renderCurrentState();
+ }
+
+ #renderCurrentState() {
+ if (!this.count || !this.groups) return;
+ if (this.state.errorMessage) {
+ this.#renderError(this.state.errorMessage);
+ return;
+ }
+ if (this.state.loaded) {
+ this.#renderTokens();
+ return;
+ }
+ this.#renderLoading();
+ }
+
+ #setTheme(theme) {
+ this.state.theme = theme;
+ document.documentElement.setAttribute('nve-theme', theme);
+ this.themeButtons.forEach((button, key) => {
+ button.toggleAttribute('pressed', key === theme);
+ });
+ }
+
+ #renderLoading() {
+ this.count.textContent = 'Loading';
+ this.groups.className = 'tokens-groups token-empty';
+ this.groups.textContent = 'Loading tokens';
+ }
+
+ #renderError(message) {
+ this.count.textContent = 'Unavailable';
+ this.groups.className = 'tokens-groups token-error';
+ this.groups.textContent = message;
+ }
+
+ #renderTokens() {
+ const filtered = this.state.query
+ ? this.state.tokens.filter(token => {
+ const query = this.state.query;
+ return (
+ token.name.toLowerCase().includes(query) ||
+ token.value.toLowerCase().includes(query) ||
+ token.description.toLowerCase().includes(query)
+ );
+ })
+ : this.state.tokens;
+
+ this.count.textContent = filtered.length + ' of ' + this.state.tokens.length;
+ this.groups.className = 'tokens-groups';
+ this.groups.replaceChildren();
+
+ if (!filtered.length) {
+ this.groups.classList.add('token-empty');
+ this.groups.textContent = 'No tokens found';
+ return;
+ }
+
+ const categories = ['color', 'spacing', 'typography', 'radius', 'shadow', 'other'];
+ categories.forEach(category => {
+ const tokens = filtered.filter(token => this.#classifyToken(token) === category);
+ if (!tokens.length) return;
+ this.groups.append(this.#createTokenSection(category, tokens));
+ });
+ }
+
+ #createTokenSection(category, tokens) {
+ const section = document.createElement('section');
+ section.className = 'token-section token-section-' + category;
+
+ const titleRow = document.createElement('div');
+ titleRow.className = 'token-title-row';
+ const heading = document.createElement('h2');
+ heading.className = 'token-heading';
+ heading.textContent = this.#categoryLabel(category);
+ const count = document.createElement('span');
+ count.className = 'token-section-count';
+ count.textContent = String(tokens.length);
+ titleRow.append(heading, count);
+
+ const grid = document.createElement('nve-grid');
+ grid.className = 'token-table token-table-' + category;
+ grid.setAttribute('container', 'flat');
+
+ const header = document.createElement('nve-grid-header');
+ [
+ ['Preview', '124px', 'center'],
+ ['Token', '280px', 'start'],
+ ['Guidance', '', 'start']
+ ].forEach(([label, width, align]) => {
+ const column = document.createElement('nve-grid-column');
+ column.textContent = label;
+ if (width) column.setAttribute('width', width);
+ column.setAttribute('column-align', align);
+ header.append(column);
+ });
+
+ grid.append(header);
+ tokens.forEach(token => grid.append(this.#createTokenRow(token, category)));
+
+ section.append(titleRow, grid);
+ return section;
+ }
+
+ #createTokenRow(token, category) {
+ const row = document.createElement('nve-grid-row');
+ row.className = 'token-row token-row-' + category;
+
+ const previewCell = document.createElement('nve-grid-cell');
+ previewCell.className = 'token-preview-cell';
+ previewCell.append(this.#createPreview(token, category));
+
+ const tokenCell = document.createElement('nve-grid-cell');
+ const copyButton = document.createElement('nve-button');
+ copyButton.className = 'token-copy-button-' + category;
+ copyButton.setAttribute('container', 'flat');
+ copyButton.setAttribute('aria-label', 'Copy ' + this.#tokenReference(token.name));
+ copyButton.title = 'Copy ' + this.#tokenReference(token.name);
+ copyButton.textContent = token.name;
+ copyButton.addEventListener('click', () => {
+ void this.#copySource(this.#tokenReference(token.name), copyButton);
+ });
+ tokenCell.append(copyButton);
+
+ const guidanceCell = document.createElement('nve-grid-cell');
+ const description = document.createElement('span');
+ description.className = 'token-description';
+ description.textContent = token.description || token.value;
+ guidanceCell.append(description);
+
+ row.append(previewCell, tokenCell, guidanceCell);
+ return row;
+ }
+
+ #createPreview(token, category) {
+ if (category === 'color') return this.#createColorPreview(token);
+ if (category === 'spacing') return this.#createRulerPreview(token);
+ if (category === 'typography') return this.#createTypePreview(token);
+ if (category === 'radius') return this.#createRadiusPreview(token);
+ if (category === 'shadow') return this.#createShadowPreview(token);
+ return this.#createOtherPreview(token);
+ }
+
+ #createColorPreview(token) {
+ const preview = document.createElement('span');
+ preview.className = 'token-preview color-preview';
+ const swatch = document.createElement('span');
+ swatch.className = 'color-swatch';
+ swatch.style.background = this.#tokenReference(token.name);
+ preview.append(swatch);
+ return preview;
+ }
+
+ #createRulerPreview(token) {
+ const preview = document.createElement('span');
+ preview.className = 'token-preview ruler-preview';
+ const fill = document.createElement('span');
+ fill.className = 'ruler-fill';
+ fill.style.width = 'clamp(2px, ' + this.#tokenReference(token.name) + ', 180px)';
+ preview.append(fill);
+ return preview;
+ }
+
+ #createTypePreview(token) {
+ const preview = document.createElement('span');
+ preview.className = 'token-preview type-preview';
+ const specimen = document.createElement('span');
+ specimen.textContent = 'Ag';
+ specimen.style.fontFamily = 'var(--nve-ref-font-family)';
+ if (token.name.includes('font-size')) {
+ specimen.style.fontSize = this.#tokenReference(token.name);
+ } else if (token.name.includes('font-weight')) {
+ specimen.style.fontWeight = this.#tokenReference(token.name);
+ specimen.style.fontSize = 'var(--nve-ref-font-size-600)';
+ } else if (token.name.includes('font-family')) {
+ specimen.style.fontFamily = this.#tokenReference(token.name);
+ specimen.style.fontSize = 'var(--nve-ref-font-size-600)';
+ }
+ preview.append(specimen);
+ return preview;
+ }
+
+ #createRadiusPreview(token) {
+ const preview = document.createElement('span');
+ preview.className = 'token-preview radius-preview';
+ const shape = document.createElement('span');
+ shape.className = 'radius-shape';
+ if (token.name.includes('border-width')) {
+ shape.style.borderWidth = this.#tokenReference(token.name);
+ } else {
+ shape.style.borderRadius = this.#tokenReference(token.name);
+ }
+ preview.append(shape);
+ return preview;
+ }
+
+ #createShadowPreview(token) {
+ const preview = document.createElement('span');
+ preview.className = 'token-preview shadow-preview';
+ const shape = document.createElement('span');
+ shape.className = 'shadow-shape';
+ shape.style.boxShadow = this.#tokenReference(token.name);
+ preview.append(shape);
+ return preview;
+ }
+
+ #createOtherPreview(token) {
+ const preview = document.createElement('span');
+ preview.className = 'token-preview type-preview token-other-preview';
+ preview.textContent = token.value;
+ return preview;
+ }
+
+ async #copySource(source, button) {
+ try {
+ if (navigator.clipboard && navigator.clipboard.writeText) {
+ await navigator.clipboard.writeText(source);
+ } else {
+ this.#copyWithTextArea(source);
+ }
+ } catch (_err) {
+ this.#copyWithTextArea(source);
+ }
+ button.dataset.copied = 'true';
+ this.#showCopiedToast(button, source);
+ window.setTimeout(() => {
+ button.dataset.copied = 'false';
+ }, 1200);
+ }
+
+ #showCopiedToast(button, source) {
+ if (!this.copiedToast) return;
+ this.copiedToast.textContent = 'Copied ' + source;
+ if (this.copiedToast.matches(':popover-open')) {
+ this.copiedToast.hidePopover();
+ }
+ this.copiedToast.showPopover({ source: button });
+ }
+
+ #copyWithTextArea(source) {
+ const textarea = document.createElement('textarea');
+ textarea.value = source;
+ textarea.setAttribute('readonly', '');
+ textarea.style.position = 'fixed';
+ textarea.style.opacity = '0';
+ document.body.append(textarea);
+ textarea.select();
+ document.execCommand('copy');
+ textarea.remove();
+ }
+
+ #setTokens(tokens) {
+ this.state.tokens = tokens;
+ this.state.errorMessage = '';
+ this.state.loaded = true;
+ this.state.loading = false;
+ this.#renderCurrentState();
+ }
+
+ #requestTokens() {
+ if (this.state.loaded || this.state.loading) return;
+ this.state.loading = true;
+ this.dispatchEvent(new CustomEvent('tokens-request', { bubbles: true, composed: true }));
+ }
+}
+
+customElements.define('nve-mcp-api-tokens-list', ElementsTokensList);
+
+const client = new Client({ name: 'Elements Token Explorer', version: '1.0.0' });
+
+const tokenList = document.createElement('nve-mcp-api-tokens-list');
+
+function getStructuredResult(payload) {
+ if (!payload) return undefined;
+ if (payload.structuredContent && payload.structuredContent.result !== undefined)
+ return payload.structuredContent.result;
+ if (payload.result && payload.result.structuredContent) return payload.result.structuredContent.result;
+ if (payload.result !== undefined) return payload.result;
+ return undefined;
+}
+
+function getTokens(payload) {
+ const result = getStructuredResult(payload);
+ if (!Array.isArray(result)) return [];
+ return result
+ .filter(token => token && typeof token.name === 'string' && typeof token.value === 'string')
+ .map(token => ({
+ name: token.name.startsWith('--') ? token.name : '--' + token.name,
+ value: token.value,
+ description: typeof token.description === 'string' ? token.description : ''
+ }));
+}
+
+function hasTokenResult(payload) {
+ return Array.isArray(getStructuredResult(payload));
+}
+
+let tokensLoaded = false;
+let tokensLoading = false;
+
+function setTokensFromPayload(payload) {
+ if (!hasTokenResult(payload)) return false;
+ tokensLoaded = true;
+ tokensLoading = false;
+ tokenList.tokens = getTokens(payload);
+ return true;
+}
+
+async function loadTokens({ force = false, silent = false } = {}) {
+ if ((!force && tokensLoaded) || tokensLoading) return;
+ tokensLoading = true;
+ try {
+ const res = await client.callServerTool({
+ name: 'api_tokens_list',
+ arguments: { format: 'json' }
+ });
+ if (!setTokensFromPayload(res)) {
+ tokensLoading = false;
+ if (!silent) {
+ tokenList.errorMessage = 'Failed to load tokens: no tokens returned';
+ }
+ }
+ } catch (err) {
+ tokensLoading = false;
+ if (!silent) {
+ tokenList.errorMessage = 'Failed to load tokens: ' + (err && err.message ? err.message : 'unknown error');
+ }
+ }
+}
+
+tokenList.addEventListener('tokens-request', () => {
+ void loadTokens();
+});
+
+client.ontoolresult = params => {
+ if (setTokensFromPayload(params)) {
+ if (tokenList.query) {
+ void loadTokens({ force: true, silent: true });
+ }
+ return;
+ }
+ void loadTokens();
+};
+
+client.ontoolinput = params => {
+ const args = params && params.arguments;
+ if (args && typeof args.query === 'string') {
+ tokenList.query = args.query;
+ }
+};
+
+client.connect();
+document.body.replaceChildren(tokenList);
diff --git a/projects/cli/src/mcp/ui/client.js b/projects/cli/src/mcp/ui/client.js
new file mode 100644
index 000000000..7ccf0dcdc
--- /dev/null
+++ b/projects/cli/src/mcp/ui/client.js
@@ -0,0 +1,115 @@
+class Client {
+ #nextId = 0;
+ #handlers = new Map();
+ #lastReportedWidth = -1;
+ #lastReportedHeight = -1;
+ #expectedParentOrigin;
+
+ constructor(info) {
+ this.name = info.name;
+ this.version = info.version;
+ }
+
+ connect() {
+ window.addEventListener('message', e => {
+ const expectedOrigin = this.#getExpectedParentOrigin();
+ if (!expectedOrigin || e.origin !== expectedOrigin || e.source !== window.parent) return;
+ const m = e.data;
+ if (!m || m.jsonrpc !== '2.0') return;
+ if (m.id !== undefined && this.#handlers.has(m.id)) {
+ const { resolve, reject } = this.#handlers.get(m.id);
+ this.#handlers.delete(m.id);
+ if (m.error) reject(new Error(m.error.message || 'rpc error'));
+ else resolve(m.result);
+ return;
+ }
+ if (m.method === 'ui/notifications/tool-input' && this.ontoolinput) this.ontoolinput(m.params || {});
+ if (m.method === 'ui/notifications/tool-result' && this.ontoolresult) this.ontoolresult(m.params || {});
+ });
+ this.#send({
+ jsonrpc: '2.0',
+ id: ++this.#nextId,
+ method: 'ui/initialize',
+ params: { name: this.name, version: this.version }
+ });
+ this.#send({ jsonrpc: '2.0', method: 'ui/notifications/initialized' });
+
+ this.#observePreferredTheme();
+ setTimeout(async () => {
+ await awaitElementUpgrades();
+ this.#observeUiSize();
+ }, 0);
+ }
+
+ callServerTool(args) {
+ return new Promise((resolve, reject) => {
+ const id = ++this.#nextId;
+ this.#handlers.set(id, { resolve, reject });
+ this.#send({ jsonrpc: '2.0', id, method: 'tools/call', params: args });
+ });
+ }
+
+ #send(msg) {
+ const expectedOrigin = this.#getExpectedParentOrigin();
+ if (!expectedOrigin) return;
+ window.parent.postMessage(msg, expectedOrigin);
+ }
+
+ #getExpectedParentOrigin() {
+ if (this.#expectedParentOrigin !== undefined) return this.#expectedParentOrigin;
+ const origin = window.location.ancestorOrigins?.[0] || (document.referrer ? new URL(document.referrer).origin : '');
+ this.#expectedParentOrigin = this.#isAllowedParentOrigin(origin) ? origin : '';
+ return this.#expectedParentOrigin;
+ }
+
+ #isAllowedParentOrigin(origin) {
+ return typeof origin === 'string' && origin !== '' && origin !== 'null';
+ }
+
+ #observePreferredTheme() {
+ const darkQuery = window.matchMedia('(prefers-color-scheme: dark)');
+ const apply = matches => {
+ document.documentElement.setAttribute('nve-theme', matches ? 'dark' : 'light');
+ };
+ apply(darkQuery.matches);
+ darkQuery.addEventListener('change', e => apply(e.matches));
+ return darkQuery;
+ }
+
+ #observeUiSize() {
+ const sizeObserver = new ResizeObserver(() => this.#reportUiSize());
+ sizeObserver.observe(document.body);
+ this.#reportUiSize();
+ return sizeObserver;
+ }
+
+ #reportUiSize() {
+ const SIZE_CHANGE_THRESHOLD = 1;
+ const width = Math.ceil(document.body.scrollWidth);
+ const measuredHeight = Math.ceil(document.body.scrollHeight);
+ const resolvedMinHeight = hasNvePage() ? 580 : 200;
+ const height = Math.max(resolvedMinHeight, measuredHeight);
+
+ const widthDelta = Math.abs(width - this.#lastReportedWidth);
+ const heightDelta = Math.abs(height - this.#lastReportedHeight);
+ if (widthDelta <= SIZE_CHANGE_THRESHOLD && heightDelta <= SIZE_CHANGE_THRESHOLD) return;
+
+ this.#lastReportedWidth = width;
+ this.#lastReportedHeight = height;
+ this.#send({ jsonrpc: '2.0', method: 'ui/notifications/size-changed', params: { width, height } });
+ }
+}
+
+function hasNvePage(root = document.body) {
+ if (root.querySelector('nve-page')) return true;
+ return Array.from(root.querySelectorAll('*')).some(el => el.shadowRoot && hasNvePage(el.shadowRoot));
+}
+
+async function awaitElementUpgrades(root = document.body) {
+ const tags = new Set();
+ root.querySelectorAll('*').forEach(el => {
+ const tag = el.tagName.toLowerCase();
+ if (tag.includes('-')) tags.add(tag);
+ });
+ await Promise.all(Array.from(tags).map(tag => customElements.whenDefined(tag).catch(() => {})));
+}
diff --git a/projects/cli/src/mcp/ui/examples-render.js b/projects/cli/src/mcp/ui/examples-render.js
new file mode 100644
index 000000000..b29ed0188
--- /dev/null
+++ b/projects/cli/src/mcp/ui/examples-render.js
@@ -0,0 +1,159 @@
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+class ElementsExamplePreview extends HTMLElement {
+ constructor() {
+ super();
+ this.state = { template: '', errorMessage: '', validationMessages: [] };
+ }
+
+ get template() {
+ return this.state.template;
+ }
+
+ set template(value) {
+ this.state.template = typeof value === 'string' ? value : '';
+ this.state.errorMessage = '';
+ this.state.validationMessages = [];
+ this.#render();
+ }
+
+ set errorMessage(value) {
+ this.state.errorMessage = typeof value === 'string' ? value : 'Failed to load example.';
+ this.state.validationMessages = [];
+ this.#render();
+ }
+
+ set validationMessages(value) {
+ this.state.validationMessages = Array.isArray(value) ? value : [];
+ this.state.errorMessage = '';
+ this.#render();
+ }
+
+ #render() {
+ this.replaceChildren();
+ if (this.state.errorMessage) {
+ this.append(this.#createMessage(this.state.errorMessage));
+ return;
+ }
+ if (this.state.validationMessages.length) {
+ this.append(this.#createValidationMessages(this.state.validationMessages));
+ return;
+ }
+ const fragment = document
+ .createRange()
+ .createContextualFragment(this.state.template || 'Example not found.
');
+ this.append(fragment);
+ }
+
+ #createMessage(message) {
+ const paragraph = document.createElement('pre');
+ paragraph.textContent = message;
+ return paragraph;
+ }
+
+ #createValidationMessages(messages) {
+ const pre = document.createElement('pre');
+ pre.textContent = messages
+ .map(message => {
+ const location = message.line && message.column ? ' (' + message.line + ':' + message.column + ')' : '';
+ return message.severity + ': ' + message.message + location;
+ })
+ .join('\n');
+ return pre;
+ }
+}
+
+customElements.define('nve-mcp-examples-render', ElementsExamplePreview);
+
+const client = new Client({ name: 'Elements Example Preview', version: '1.0.0' });
+
+const preview = document.createElement('nve-mcp-examples-render');
+let pendingId = null;
+let pendingTemplate = null;
+
+function setPendingExample({ arguments: args } = {}) {
+ pendingId = null;
+ pendingTemplate = null;
+ if (args && typeof args.template === 'string') {
+ pendingTemplate = args.template;
+ } else if (args && typeof args.id === 'string') {
+ pendingId = args.id;
+ }
+}
+
+function getStructuredResult(payload) {
+ const content = getStructuredContent(payload);
+ return content && content.result !== undefined ? content.result : content;
+}
+
+function getStructuredContent(payload) {
+ if (!payload) return undefined;
+ if (payload.structuredContent) return payload.structuredContent;
+ if (payload.result && payload.result.structuredContent) return payload.result.structuredContent;
+ if (payload.result !== undefined) return payload.result;
+ return undefined;
+}
+
+function getToolErrorMessage(payload) {
+ const content = getStructuredContent(payload);
+ return content && content.status === 'error' && typeof content.message === 'string' ? content.message : '';
+}
+
+function getExampleTemplate(payload) {
+ const example = getStructuredResult(payload);
+ return example && typeof example.template === 'string' ? example.template : '';
+}
+
+function getRenderResult(payload) {
+ const result = getStructuredResult(payload);
+ if (!result || typeof result.template !== 'string' || !Array.isArray(result.lintMessages)) return undefined;
+ return result;
+}
+
+async function renderPendingExample(params) {
+ const renderResult = getRenderResult(params);
+ if (renderResult) {
+ if (renderResult.lintMessages.length) {
+ preview.validationMessages = renderResult.lintMessages;
+ return;
+ }
+ preview.template = renderResult.template;
+ return;
+ }
+ const errorMessage = getToolErrorMessage(params);
+ if (errorMessage) {
+ preview.errorMessage = errorMessage;
+ return;
+ }
+ if (typeof pendingTemplate === 'string') {
+ preview.errorMessage = 'Failed to validate template.';
+ return;
+ }
+ if (!pendingId) return;
+ try {
+ const res = await client.callServerTool({
+ name: 'examples_get',
+ arguments: { id: pendingId, format: 'json' }
+ });
+ const exampleErrorMessage = getToolErrorMessage(res);
+ if (exampleErrorMessage) {
+ preview.errorMessage = exampleErrorMessage;
+ return;
+ }
+ preview.template = getExampleTemplate(res);
+ } catch (err) {
+ preview.errorMessage = 'Failed to load example: ' + (err && err.message ? err.message : 'unknown error');
+ }
+}
+
+client.ontoolinput = params => {
+ setPendingExample(params);
+};
+
+client.ontoolresult = params => {
+ void renderPendingExample(params);
+};
+
+client.connect();
+document.body.replaceChildren(preview);
diff --git a/projects/cli/src/mcp/ui/index.test.ts b/projects/cli/src/mcp/ui/index.test.ts
new file mode 100644
index 000000000..0b0f62090
--- /dev/null
+++ b/projects/cli/src/mcp/ui/index.test.ts
@@ -0,0 +1,25 @@
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { describe, expect, it } from 'vitest';
+import { buildUIResourceHtml } from './index.js';
+
+describe(buildUIResourceHtml.name, () => {
+ it('should validate inbound MCP UI message source and origin before reading data', () => {
+ const html = buildUIResourceHtml({
+ title: 'Test MCP UI',
+ script: ''
+ });
+ const messageListenerScript = html.slice(
+ html.indexOf("window.addEventListener('message'"),
+ html.indexOf('if (m.id !== undefined')
+ );
+
+ expect(messageListenerScript).toContain('const expectedOrigin = this.#getExpectedParentOrigin();');
+ expect(messageListenerScript).toContain('e.origin !== expectedOrigin');
+ expect(messageListenerScript).toContain('e.source !== window.parent');
+ expect(messageListenerScript.indexOf('const expectedOrigin')).toBeLessThan(
+ messageListenerScript.indexOf('const m = e.data')
+ );
+ });
+});
diff --git a/projects/cli/src/mcp/ui/index.ts b/projects/cli/src/mcp/ui/index.ts
new file mode 100644
index 000000000..115d6c632
--- /dev/null
+++ b/projects/cli/src/mcp/ui/index.ts
@@ -0,0 +1,101 @@
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import coreBundle from '@nvidia-elements/core/bundles/index.js?raw';
+import codeBundle from '@nvidia-elements/code/bundles/index.js?raw';
+import themesIndexCss from '@nvidia-elements/themes/index.css?raw';
+import themesDarkCss from '@nvidia-elements/themes/dark.css?raw';
+import themesHighContrastCss from '@nvidia-elements/themes/high-contrast.css?raw';
+import stylesTypographyCss from '@nvidia-elements/styles/typography.css?raw';
+import stylesLayoutCss from '@nvidia-elements/styles/layout.css?raw';
+import clientScript from './client.js?raw';
+import iconsListScript from './api-icons-list.js?raw';
+import tokensListScript from './api-tokens-list.js?raw';
+import examplesRenderScript from './examples-render.js?raw';
+
+export interface UIResource {
+ name: string;
+ mimeType: string;
+ resourceUri: string;
+ description: string;
+ getHtml: () => string;
+}
+
+interface UIBundles {
+ code?: boolean;
+}
+
+export const MCP_UI_MIME_TYPE = 'text/html;profile=mcp-app';
+
+export const examplesRenderResource: UIResource = {
+ name: 'nve-mcp-examples-render',
+ mimeType: MCP_UI_MIME_TYPE,
+ resourceUri: 'ui://elements/example-preview',
+ description: 'Renders Elements component examples in a sandboxed iframe.',
+ getHtml: () =>
+ buildUIResourceHtml({
+ title: 'Elements Example Preview',
+ script: examplesRenderScript
+ })
+};
+
+export const iconsListResource: UIResource = {
+ name: 'nve-mcp-api-icons-list',
+ mimeType: MCP_UI_MIME_TYPE,
+ resourceUri: 'ui://elements/icons-list',
+ description: 'Renders available Elements icons with copyable source markup.',
+ getHtml: () =>
+ buildUIResourceHtml({
+ title: 'Elements Icons List',
+ script: iconsListScript
+ })
+};
+
+export const tokensListResource: UIResource = {
+ name: 'nve-mcp-api-tokens-list',
+ mimeType: MCP_UI_MIME_TYPE,
+ resourceUri: 'ui://elements/tokens-list',
+ description: 'Renders Elements design tokens with visual previews and copyable CSS references.',
+ getHtml: () =>
+ buildUIResourceHtml({
+ title: 'Elements Token Explorer',
+ script: tokensListScript
+ })
+};
+
+export const uiResources: UIResource[] = [examplesRenderResource, iconsListResource, tokensListResource];
+
+const getBundles = ({ code = false }: UIBundles = {}): string => {
+ const scripts = code ? [coreBundle, codeBundle] : [coreBundle];
+ return scripts.map(script => /* html */ ``).join('\n');
+};
+
+export function buildUIResourceHtml({
+ title,
+ bundles,
+ script
+}: {
+ title: string;
+ script: string;
+ bundles?: UIBundles;
+}): string {
+ return /* html */ `
+
+
+
+ ${title}
+
+
+
+ ${getBundles(bundles)}
+
+
+`;
+}
diff --git a/projects/cli/src/types.d.ts b/projects/cli/src/types.d.ts
index 1b8256ea2..721038419 100644
--- a/projects/cli/src/types.d.ts
+++ b/projects/cli/src/types.d.ts
@@ -5,3 +5,8 @@ declare module 'marked-terminal' {
import type { MarkedExtension } from 'marked';
export function markedTerminal(options?: Record): MarkedExtension;
}
+
+declare module '*?raw' {
+ const content: string;
+ export default content;
+}
diff --git a/projects/cli/src/utils.test.ts b/projects/cli/src/utils.test.ts
index a6f926689..5e1479c6c 100644
--- a/projects/cli/src/utils.test.ts
+++ b/projects/cli/src/utils.test.ts
@@ -20,7 +20,8 @@ import {
isObjectLiteral,
renderResult,
statusIcons,
- wrapUrl
+ wrapUrl,
+ progressBar
} from './utils.js';
vi.mock('ora');
@@ -162,6 +163,22 @@ describe('utils', () => {
await expect(runAsyncTool(args, mockFn)).rejects.toThrow('Test error');
expect(mockFn).toHaveBeenCalledWith(args);
});
+
+ it('should append console and progress output to the spinner text', async () => {
+ vi.spyOn(Date, 'now').mockReturnValueOnce(0).mockReturnValueOnce(1000);
+ const args = {};
+ mockFn.mockImplementationOnce(async value => {
+ console.log('console update');
+ (value.onProgress as (message: string) => void)('progress update');
+ return 'test result';
+ });
+
+ const result = await runAsyncTool(args, mockFn);
+
+ expect(result).toBe('test result');
+ expect(mockSpinner.text).toContain('console update');
+ expect(mockSpinner.text).toContain('progress update');
+ });
});
describe('getArgValue', async () => {
@@ -366,6 +383,18 @@ describe('utils', () => {
});
});
+ describe('progressBar', () => {
+ it('should render a clamped progress bar with default width', () => {
+ expect(progressBar(50)).toBe('██████████░░░░░░░░░░');
+ expect(progressBar(-10)).toBe('░░░░░░░░░░░░░░░░░░░░');
+ expect(progressBar(110)).toBe('████████████████████');
+ });
+
+ it('should respect custom width', () => {
+ expect(progressBar(25, 8)).toBe('██░░░░░░');
+ });
+ });
+
describe('isReport', () => {
it('should return true for valid report objects', () => {
const validReport = {
diff --git a/projects/cli/vitest.config.ts b/projects/cli/vitest.config.ts
index 02afeeec7..aa4f66030 100644
--- a/projects/cli/vitest.config.ts
+++ b/projects/cli/vitest.config.ts
@@ -10,6 +10,7 @@ export default mergeConfig(libraryNodeTestConfig, {
test: {
include: ['./src/**/*.test.ts'],
coverage: {
+ exclude: ['src/mcp/ui/*.js'],
thresholds: {
lines: 90,
branches: 90,
diff --git a/projects/code/src/codeblock/codeblock.test.lighthouse.ts b/projects/code/src/codeblock/codeblock.test.lighthouse.ts
index 42b6df16f..2fac6ec16 100644
--- a/projects/code/src/codeblock/codeblock.test.lighthouse.ts
+++ b/projects/code/src/codeblock/codeblock.test.lighthouse.ts
@@ -18,6 +18,6 @@ describe('codeblock lighthouse report', () => {
expect(report.scores.performance).toBe(100);
expect(report.scores.accessibility).toBe(100);
expect(report.scores.bestPractices).toBe(100);
- expect(report.payload.javascript.kb).toBeLessThan(19.6);
+ expect(report.payload.javascript.kb).toBeLessThan(19.7);
});
});
diff --git a/projects/core/src/button/button.css b/projects/core/src/button/button.css
index 8cecce220..826530a29 100644
--- a/projects/core/src/button/button.css
+++ b/projects/core/src/button/button.css
@@ -144,7 +144,7 @@
--color: var(--nve-sys-text-white-color);
}
-:host(:hover:not([interaction], [disabled], [readonly])) {
+:host(:hover:not([interaction], [disabled], [readonly], [pressed])) {
--color: var(--nve-sys-interaction-hover-color);
}
diff --git a/projects/core/src/internal/services/global.utils.ts b/projects/core/src/internal/services/global.utils.ts
index 43c4ec202..fd0fb2d13 100644
--- a/projects/core/src/internal/services/global.utils.ts
+++ b/projects/core/src/internal/services/global.utils.ts
@@ -1,8 +1,6 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
-import { getEsmHostedWarning } from '../utils/audit-logs.js';
-
declare global {
var process: { env?: { NODE_ENV?: string } } | undefined;
}
@@ -22,11 +20,6 @@ export function getEnv(): 'development' | 'production' {
export function getHostDetails(): { moduleHost: string; pageHost: string } {
const pageHost = globalThis.location?.hostname;
const moduleHost = new URL(import.meta.url).hostname;
- const isEsmHosted = moduleHost.startsWith('esm.');
-
- if (isEsmHosted && pageHost !== 'localhost' && !pageHost.includes('esm.')) {
- console.warn(getEsmHostedWarning());
- }
return {
pageHost,
diff --git a/projects/core/src/internal/utils/audit-logs.ts b/projects/core/src/internal/utils/audit-logs.ts
index a78ad1976..173d8b83a 100644
--- a/projects/core/src/internal/utils/audit-logs.ts
+++ b/projects/core/src/internal/utils/audit-logs.ts
@@ -36,7 +36,3 @@ export function getDuplicatePackageVersionWarning(localName: string, version: st
export function getDuplicatePackageGlobalVersionWarning() {
return `@nve: Multiple versions of Elements loaded, please check for duplicate package versions. ${DOCS_LOG_URL}#duplicate-package-version`;
}
-
-export function getEsmHostedWarning() {
- return '@nve: Using esm.sh is not supported for production use.';
-}
diff --git a/projects/internals/tools/src/api/service.test.ts b/projects/internals/tools/src/api/service.test.ts
index df282a291..ca2f6c7ca 100644
--- a/projects/internals/tools/src/api/service.test.ts
+++ b/projects/internals/tools/src/api/service.test.ts
@@ -121,25 +121,22 @@ describe('ApiService', () => {
expect(result[0].name).toBe('nve-button');
});
- it('should return helpful message when all names are not found', async () => {
- const result = await ApiService.get({
- names: ['nve-nonexistent-abc', 'nve-nonexistent-xyz'],
- format: 'markdown'
- });
- expect(typeof result).toBe('string');
- expect(result as string).toContain('No components or APIs found matching');
- expect(result as string).toContain('nve-nonexistent-abc');
- expect(result as string).toContain('nve-nonexistent-xyz');
- expect(result as string).toContain('Tip:');
+ it('should reject when all names are not found', async () => {
+ await expect(
+ ApiService.get({
+ names: ['nve-nonexistent-abc', 'nve-nonexistent-xyz'],
+ format: 'markdown'
+ })
+ ).rejects.toThrow('No components or APIs found matching "nve-nonexistent-abc", "nve-nonexistent-xyz".');
});
- it('should return helpful message when all names are not found (json)', async () => {
- const result = await ApiService.get({
- names: ['nve-nonexistent-abc'],
- format: 'json'
- });
- expect(typeof result).toBe('string');
- expect(result as string).toContain('No components or APIs found matching');
+ it('should reject when all names are not found (json)', async () => {
+ await expect(
+ ApiService.get({
+ names: ['nve-nonexistent-abc'],
+ format: 'json'
+ })
+ ).rejects.toThrow('No components or APIs found matching "nve-nonexistent-abc".');
});
});
@@ -212,6 +209,7 @@ describe('ApiService', () => {
const result = await MockedApiService.templateValidate({ template: 'Test' });
expect(Array.isArray(result)).toBe(true);
+ expect(mockLintTemplate).toHaveBeenCalledWith('Test', { strict: false });
vi.doUnmock('@nvidia-elements/lint/eslint/internals');
});
@@ -227,6 +225,7 @@ describe('ApiService', () => {
const result = await MockedApiService.templateValidate({ template: 'Test' });
expect(Array.isArray(result)).toBe(true);
+ expect(mockLintTemplate).toHaveBeenCalledWith('Test', { strict: false });
vi.doUnmock('@nvidia-elements/lint/eslint/internals');
});
});
@@ -263,6 +262,10 @@ describe('ApiService', () => {
expect((ApiService.tokensList as ToolMethod).metadata.summary).toBe(
'Get available semantic CSS custom properties / design tokens for theming.'
);
+ expect((ApiService.tokensList as ToolMethod).metadata.inputSchema?.properties?.query).toBeDefined();
+ expect((ApiService.tokensList as ToolMethod).metadata.app?.resourceUri).toBe(
+ 'ui://elements/tokens-list'
+ );
});
it('should provide list tool', async () => {
@@ -270,6 +273,18 @@ describe('ApiService', () => {
expect(result).toBeDefined();
expect(result).toContain('## CSS Variables');
});
+
+ it('should filter tokens by query', async () => {
+ const result = (await ApiService.tokensList({ format: 'json', query: 'shadow' })) as {
+ name: string;
+ value: string;
+ description: string;
+ }[];
+ expect(result.length).toBeGreaterThan(0);
+ expect(
+ result.every(token => [token.name, token.value, token.description].join(' ').toLowerCase().includes('shadow'))
+ ).toBe(true);
+ });
});
describe('iconsList', () => {
@@ -279,6 +294,7 @@ describe('ApiService', () => {
expect((ApiService.iconsList as ToolMethod).metadata.summary).toBe(
'Get list of all available icon names for nve-icon and nve-icon-button.'
);
+ expect((ApiService.iconsList as ToolMethod).metadata.app?.resourceUri).toBe('ui://elements/icons-list');
});
it('should return markdown with all icon names', async () => {
diff --git a/projects/internals/tools/src/api/service.ts b/projects/internals/tools/src/api/service.ts
index 0a8b41e7a..0f35dcb19 100644
--- a/projects/internals/tools/src/api/service.ts
+++ b/projects/internals/tools/src/api/service.ts
@@ -122,7 +122,7 @@ export class ApiService {
const notFound = results.filter((r: Element | Attribute | string): r is string => typeof r === 'string');
if (found.length === 0) {
- return `No components or APIs found matching "${notFound.join('", "')}".\n\n${listToolHelpfulTip}`;
+ throw new Error(`No components or APIs found matching "${notFound.join('", "')}".\n\n${listToolHelpfulTip}`);
}
if (format === 'json') {
@@ -162,12 +162,8 @@ export class ApiService {
}
})
static async templateValidate({ template }: { template: string }): Promise {
- if (process.env.ELEMENTS_ENV === 'mcp' || process.env.ELEMENTS_ENV === 'cli') {
- const { lintTemplate } = await import('@nvidia-elements/lint/eslint/internals');
- return await lintTemplate(template);
- } else {
- return [];
- }
+ const { lintTemplate } = await import('@nvidia-elements/lint/eslint/internals');
+ return await lintTemplate(template, { strict: false });
}
@tool({
@@ -192,6 +188,7 @@ export class ApiService {
}
@tool({
+ app: { resourceUri: 'ui://elements/tokens-list' },
summary: 'Get available semantic CSS custom properties / design tokens for theming.',
inputSchema: {
type: 'object',
@@ -201,6 +198,10 @@ export class ApiService {
description: markdownDescription,
enum: ['markdown', 'json'],
default: 'markdown'
+ },
+ query: {
+ type: 'string',
+ description: 'Optional search query used to filter tokens by name, value, or description before rendering.'
}
},
additionalProperties: false
@@ -225,13 +226,14 @@ export class ApiService {
}
})
static async tokensList(
- { format }: { format: 'markdown' | 'json' } = { format: 'markdown' }
- ): Promise<{ name: string; description: string }[] | string> {
+ { format, query }: { format: 'markdown' | 'json'; query?: string } = { format: 'markdown' }
+ ): Promise<{ name: string; value: string; description: string }[] | string> {
const apis = await MetadataApiService.getData();
- return getContextTokens(format, apis.data.tokens) ?? '';
+ return getContextTokens(format, apis.data.tokens, { query }) ?? '';
}
@tool({
+ app: { resourceUri: 'ui://elements/icons-list' },
summary: 'Get list of all available icon names for nve-icon and nve-icon-button.',
inputSchema: {
type: 'object',
diff --git a/projects/internals/tools/src/api/utils.test.ts b/projects/internals/tools/src/api/utils.test.ts
index 218ebcb1f..dbd4b5d5b 100644
--- a/projects/internals/tools/src/api/utils.test.ts
+++ b/projects/internals/tools/src/api/utils.test.ts
@@ -350,6 +350,21 @@ describe('getContextTokens', () => {
const result = getContextTokens('json', tokens) as Token[];
expect(result).toHaveLength(3);
});
+
+ it('should filter tokens by query across name, value, and description', () => {
+ const tokens = [
+ createToken('sys-color-primary', '#007bff', 'Primary action background'),
+ createToken('sys-spacing-md', '16px', 'Comfortable layout gap'),
+ createToken('sys-font-size-lg', '24px', 'Large heading text')
+ ];
+ const nameResult = getContextTokens('json', tokens, { query: 'spacing' }) as Token[];
+ const valueResult = getContextTokens('json', tokens, { query: '24px' }) as Token[];
+ const descriptionResult = getContextTokens('json', tokens, { query: 'ACTION' }) as Token[];
+
+ expect(nameResult.map(token => token.name)).toEqual(['sys-spacing-md']);
+ expect(valueResult.map(token => token.name)).toEqual(['sys-font-size-lg']);
+ expect(descriptionResult.map(token => token.name)).toEqual(['sys-color-primary']);
+ });
});
describe('sorting', () => {
diff --git a/projects/internals/tools/src/api/utils.ts b/projects/internals/tools/src/api/utils.ts
index ca5cf521b..01dd299d8 100644
--- a/projects/internals/tools/src/api/utils.ts
+++ b/projects/internals/tools/src/api/utils.ts
@@ -59,8 +59,22 @@ export async function searchContextAPIs(query: string, config: { limit?: number
return config.limit !== undefined ? data.slice(0, config.limit) : data;
}
-export function getContextTokens(format: 'markdown' | 'json', tokens: Token[]): string | Token[] | undefined {
- const filteredTokens = distillTokens(tokens);
+interface TokenFilterOptions {
+ query?: string;
+}
+
+function tokenMatchesQuery(token: Token, query: string): boolean {
+ const normalizedQuery = query.trim().toLowerCase();
+ if (!normalizedQuery) return true;
+ return [token.name, token.value, token.description].some(value => value.toLowerCase().includes(normalizedQuery));
+}
+
+export function getContextTokens(
+ format: 'markdown' | 'json',
+ tokens: Token[],
+ { query = '' }: TokenFilterOptions = {}
+): string | Token[] | undefined {
+ const filteredTokens = distillTokens(tokens).filter(token => tokenMatchesQuery(token, query));
if (format === 'markdown') {
return `## CSS Variables\n\nAvailable semantic design tokens for theming.
diff --git a/projects/internals/tools/src/distill/examples.ts b/projects/internals/tools/src/distill/examples.ts
index 098fd37b7..442d14183 100644
--- a/projects/internals/tools/src/distill/examples.ts
+++ b/projects/internals/tools/src/distill/examples.ts
@@ -27,14 +27,18 @@ function passesExclusionChecks(example: Partial) {
}
function passesInclusionChecks(example: Partial) {
- if (example.composition) {
- return true;
- }
+ const { summary, composition } = example;
const id = example.id ?? '';
const tags = example.tags ?? [];
+ const isDefault = !id.includes('-'); // default is only the element/api name, no suffix
+
+ if (composition || isDefault) {
+ return true;
+ }
+
return (
(includedIdPatterns.some(p => id.includes(p)) || tags.includes('pattern') || tags.includes('template')) &&
- (example.summary?.length ?? 0) > 0
+ (summary?.length ?? 0) > 0
);
}
diff --git a/projects/internals/tools/src/examples/service.test.ts b/projects/internals/tools/src/examples/service.test.ts
index b330123c6..2b57f4bd7 100644
--- a/projects/internals/tools/src/examples/service.test.ts
+++ b/projects/internals/tools/src/examples/service.test.ts
@@ -1,13 +1,18 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
-import { describe, expect, it } from 'vitest';
+import { afterEach, describe, expect, it, vi } from 'vitest';
import type { Example } from '@internals/metadata';
import { ExamplesService } from './service.js';
import { getContextExamples } from './utils.js';
-import type { ToolMethod } from '../internal/tools.js';
+import { loadTools, type ToolMethod, type ToolOutput } from '../internal/tools.js';
+import { ApiService } from '../api/service.js';
describe('ExampleService', () => {
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
it('should provide list tool', async () => {
const result = await ExamplesService.list();
expect(result).toBeDefined();
@@ -80,14 +85,31 @@ describe('ExampleService', () => {
expect(result.id.toLowerCase()).toBe(examples[0].id.toLowerCase());
});
- it('should return not-found message for unknown example id (markdown)', async () => {
- const result = await ExamplesService.get({ id: 'nonexistent-example-xyz', format: 'markdown' });
- expect(result).toBe('Example not found. Use the list tool to get a list of all available examples and patterns.');
+ it('should reject unknown example id (markdown)', async () => {
+ await expect(ExamplesService.get({ id: 'nonexistent-example-xyz', format: 'markdown' })).rejects.toThrow(
+ 'Unknown example "nonexistent-example-xyz". Use the examples_list tool to get a list of all available examples.'
+ );
});
- it('should return undefined for unknown example id (json)', async () => {
- const result = await ExamplesService.get({ id: 'nonexistent-example-xyz', format: 'json' });
- expect(result).toBeUndefined();
+ it('should reject unknown example id (json)', async () => {
+ await expect(ExamplesService.get({ id: 'nonexistent-example-xyz', format: 'json' })).rejects.toThrow(
+ 'Unknown example "nonexistent-example-xyz". Use the examples_list tool to get a list of all available examples.'
+ );
+ });
+
+ it('should return a managed tool error for unknown example id', async () => {
+ const tools = loadTools(ExamplesService);
+ const getTool = tools.find(tool => tool.metadata.name === 'get');
+
+ const result = (await getTool?.({ id: 'nonexistent-example-xyz', format: 'json' })) as ToolOutput<
+ Example | undefined
+ >;
+
+ expect(result.status).toBe('error');
+ expect(result.message).toBe(
+ 'Unknown example "nonexistent-example-xyz". Use the examples_list tool to get a list of all available examples.'
+ );
+ expect(result.result).toBeUndefined();
});
it('should provide getAll tool for internal usage', async () => {
@@ -101,4 +123,70 @@ describe('ExampleService', () => {
expect(result).toContain('nonexistent-example-xyz');
expect(result).toContain('Tip:');
});
+
+ it('should provide render tool that echoes a validated template', async () => {
+ const template = 'hello';
+ const result = await ExamplesService.render({ template, name: 'Hello button' });
+ expect(result.template).toBe(template);
+ expect(result.name).toBe('Hello button');
+ expect(Array.isArray(result.lintMessages)).toBe(true);
+ expect((ExamplesService.render as ToolMethod).metadata.name).toBe('render');
+ expect((ExamplesService.render as ToolMethod).metadata.app?.resourceUri).toBe(
+ 'ui://elements/example-preview'
+ );
+ expect((ExamplesService.render as ToolMethod).metadata.inputSchema?.required).toContain('template');
+ });
+
+ it('should accept render tool without an explicit name', async () => {
+ const result = await ExamplesService.render({ template: 'x' });
+ expect(result.template).toBe('x');
+ expect(result.name).toBeUndefined();
+ });
+
+ it('should reject render tool templates with lint messages', async () => {
+ vi.spyOn(ApiService, 'templateValidate').mockResolvedValue([
+ {
+ id: 'invalid-template',
+ severity: 'error',
+ message: 'Invalid template.',
+ suggestions: [],
+ line: 1,
+ column: 2,
+ endLine: 1,
+ endColumn: 3
+ }
+ ]);
+
+ await expect(ExamplesService.render({ template: '' })).rejects.toThrow(
+ 'Template validation failed.'
+ );
+ });
+
+ it('should return a managed tool error when render validation fails', async () => {
+ vi.spyOn(ApiService, 'templateValidate').mockResolvedValue([
+ {
+ id: 'invalid-template',
+ severity: 'error',
+ message: 'Invalid template.',
+ suggestions: [],
+ line: 1,
+ column: 2,
+ endLine: 1,
+ endColumn: 3
+ }
+ ]);
+ const tools = loadTools(ExamplesService);
+ const renderTool = tools.find(tool => tool.metadata.name === 'render');
+
+ const result = (await renderTool?.({ template: '' })) as ToolOutput<{
+ template: string;
+ lintMessages: { message: string }[];
+ }>;
+
+ expect(result.status).toBe('error');
+ expect(result.message).toBe('Template validation failed.');
+ expect(result.result?.template).toBe('');
+ expect(result.result?.lintMessages).toHaveLength(1);
+ expect(result.result?.lintMessages[0]?.message).toBe('Unexpected use of unknown tag ');
+ });
});
diff --git a/projects/internals/tools/src/examples/service.ts b/projects/internals/tools/src/examples/service.ts
index b99912a0a..c5cc4b5e3 100644
--- a/projects/internals/tools/src/examples/service.ts
+++ b/projects/internals/tools/src/examples/service.ts
@@ -2,9 +2,11 @@
// SPDX-License-Identifier: Apache-2.0
import { ExamplesService as ExamplesServiceMetadata, type Example } from '@internals/metadata';
-import { service, tool, ToolSupport } from '../internal/tools.js';
+import type { TemplateLintMessage } from '@nvidia-elements/lint/eslint/internals';
+import { service, tool, ToolError, ToolSupport } from '../internal/tools.js';
import { getContextExamples, renderExampleMarkdown, searchContextExamples } from './utils.js';
import { markdownDescription } from '../internal/utils.js';
+import { eslintSchema } from '../internal/schema.js';
const MAX_RESULT_LIMIT = 5;
@@ -58,6 +60,7 @@ export class ExamplesService {
}
@tool({
+ app: { resourceUri: 'ui://elements/example-preview' },
summary: 'Get the full template of a known example or pattern by id.',
inputSchema: {
type: 'object',
@@ -80,18 +83,19 @@ export class ExamplesService {
oneOf: [{ type: 'string' }, { type: 'object', additionalProperties: true }]
}
})
- static async get({ id, format }: { id: string; format: 'markdown' | 'json' }): Promise {
+ static async get({ id, format }: { id: string; format: 'markdown' | 'json' }): Promise {
const results = (await getContextExamples('json', await ExamplesServiceMetadata.getData())) as Example[];
const found = results.find(r => r.id.toLocaleLowerCase() === id.toLowerCase());
+ if (!found) {
+ throw new Error(`Unknown example "${id}". Use the examples_list tool to get a list of all available examples.`);
+ }
+
if (format === 'json') {
return found;
}
- const markdown = found?.template
- ? renderExampleMarkdown(found)
- : 'Example not found. Use the list tool to get a list of all available examples and patterns.';
- return markdown;
+ return renderExampleMarkdown(found);
}
@tool({
@@ -144,6 +148,56 @@ export class ExamplesService {
return await searchContextExamples(query, { format, limit: MAX_RESULT_LIMIT });
}
+ @tool({
+ app: { resourceUri: 'ui://elements/example-preview' },
+ support: ToolSupport.MCP,
+ summary: 'Render a custom Elements (nve-*) HTML template inline in the chat UI.',
+ description: `Render an ad-hoc Elements (nve-*) HTML template in the MCP App preview iframe. The template is linted against Elements APIs before rendering. Lint failures return a tool error instead of rendering the template. Use this to preview compositions you author. Templates should be self-contained HTML — the iframe loads the Elements bundle and themes automatically.`,
+ inputSchema: {
+ type: 'object',
+ properties: {
+ template: {
+ type: 'string',
+ description: 'HTML template to render. Should use Elements (nve-*) components.'
+ },
+ name: {
+ type: 'string',
+ description: 'Optional human-readable label for the preview.'
+ }
+ },
+ required: ['template'],
+ additionalProperties: false
+ },
+ outputSchema: {
+ type: 'object',
+ properties: {
+ template: { type: 'string' },
+ name: { type: 'string' },
+ lintMessages: {
+ type: 'array',
+ items: eslintSchema,
+ description: 'Lint messages. Empty on success; non-empty when returned with status error.'
+ }
+ },
+ additionalProperties: false,
+ required: ['template', 'lintMessages']
+ }
+ })
+ static async render({
+ template,
+ name
+ }: {
+ template: string;
+ name?: string;
+ }): Promise<{ template: string; name?: string; lintMessages: TemplateLintMessage[] }> {
+ const { lintTemplate } = await import('@nvidia-elements/lint/eslint/internals');
+ const lintMessages = await lintTemplate(template, { strict: true });
+ if (lintMessages.length) {
+ throw new ToolError('Template validation failed.', { template, name, lintMessages });
+ }
+ return { template, name, lintMessages };
+ }
+
static async getAll(): Promise {
return ExamplesServiceMetadata.getData();
}
diff --git a/projects/internals/tools/src/index.test.ts b/projects/internals/tools/src/index.test.ts
index 7462e7044..9882c2833 100644
--- a/projects/internals/tools/src/index.test.ts
+++ b/projects/internals/tools/src/index.test.ts
@@ -1,15 +1,48 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
-import { describe, expect, it } from 'vitest';
-import { tools } from './index.js';
+import { afterEach, describe, expect, it, vi } from 'vitest';
+import type { ManagedToolMethod } from './internal/tools.js';
+
+const originalPlaygroundBaseUrl = process.env.ELEMENTS_PLAYGROUND_BASE_URL;
+
+async function getTools(): Promise[]> {
+ vi.resetModules();
+ const { tools } = await import('./index.js');
+ return tools;
+}
+
+async function getToolNames(): Promise {
+ return (await getTools()).map(tool => tool.metadata.toolName);
+}
describe('tools', () => {
- it('should be defined', () => {
- expect(tools).toBeDefined();
+ afterEach(() => {
+ if (originalPlaygroundBaseUrl === undefined) {
+ delete process.env.ELEMENTS_PLAYGROUND_BASE_URL;
+ } else {
+ process.env.ELEMENTS_PLAYGROUND_BASE_URL = originalPlaygroundBaseUrl;
+ }
+ vi.resetModules();
+ });
+
+ it('should be defined', async () => {
+ await expect(getToolNames()).resolves.toBeDefined();
});
- it('should include skills tools', () => {
+ it('should omit playground tools without runtime playground url configuration', async () => {
+ delete process.env.ELEMENTS_PLAYGROUND_BASE_URL;
+ await expect(getToolNames()).resolves.not.toContain('playground_create');
+ });
+
+ it('should include playground tools with runtime playground url configuration', async () => {
+ process.env.ELEMENTS_PLAYGROUND_BASE_URL = 'https://playground.example.com';
+ await expect(getToolNames()).resolves.toContain('playground_create');
+ });
+
+ it('should include skills tools', async () => {
+ const tools = await getTools();
+
expect(tools.some(tool => tool.metadata.command === 'skills.list')).toBe(true);
expect(tools.some(tool => tool.metadata.command === 'skills.get')).toBe(true);
});
diff --git a/projects/internals/tools/src/index.ts b/projects/internals/tools/src/index.ts
index 826715dd0..dadbaa465 100644
--- a/projects/internals/tools/src/index.ts
+++ b/projects/internals/tools/src/index.ts
@@ -10,8 +10,6 @@ import { CliService } from './cli/service.js';
import { SkillsService } from './skills/service.js';
import { loadTools } from './internal/tools.js';
-declare const __ELEMENTS_PLAYGROUND_BASE_URL__: string;
-
export const VERSION = '0.0.0';
export {
@@ -20,7 +18,10 @@ export {
type ManagedToolMethod,
type Schema,
type ToolSupportFlags,
+ type ToolApp,
+ type ToolMetadata,
ToolSupport,
+ ToolError,
jsonSchemaToZod
} from './internal/tools.js';
@@ -31,7 +32,7 @@ export { type Report, type ReportCheck } from './internal/types.js';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const services: any[] = [ApiService, CliService, ExamplesService, ProjectService, PackagesService, SkillsService];
-if (__ELEMENTS_PLAYGROUND_BASE_URL__) {
+if (process.env.ELEMENTS_PLAYGROUND_BASE_URL) {
services.push(PlaygroundService);
}
diff --git a/projects/internals/tools/src/internal/tools.test.ts b/projects/internals/tools/src/internal/tools.test.ts
index 44c412888..e10803a9a 100644
--- a/projects/internals/tools/src/internal/tools.test.ts
+++ b/projects/internals/tools/src/internal/tools.test.ts
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
import { describe, expect, it } from 'vitest';
-import { loadTools, service, tool, jsonSchemaToZod, ToolSupport } from './tools.js';
+import { loadTools, service, tool, jsonSchemaToZod, ToolError, ToolSupport } from './tools.js';
import type { ToolMethod, ToolOutput, Schema } from './tools.js';
describe('metadata', () => {
@@ -179,6 +179,27 @@ describe('loadTools', () => {
expect(result).toBe(undefined);
});
+ it('should include structured result from a tool error', async () => {
+ @service()
+ class TestService {
+ @tool({ summary: 'test' })
+ static foo() {
+ return new Promise(() => {
+ throw new ToolError('error message', [{ message: 'lint error' }]);
+ });
+ }
+ }
+
+ loadTools(TestService);
+
+ const { result, status, message } = (await TestService['tool_foo']()) as unknown as ToolOutput<
+ { message: string }[]
+ >;
+ expect(status).toBe('error');
+ expect(message).toBe('error message');
+ expect(result).toEqual([{ message: 'lint error' }]);
+ });
+
it('should load multiple tools from a class', () => {
@service()
class TestService {
diff --git a/projects/internals/tools/src/internal/tools.ts b/projects/internals/tools/src/internal/tools.ts
index 7cecc8382..9d9213af5 100644
--- a/projects/internals/tools/src/internal/tools.ts
+++ b/projects/internals/tools/src/internal/tools.ts
@@ -19,7 +19,7 @@ export const ToolSupport = {
export type ToolSupportFlags = number;
-interface ToolMetadata {
+export interface ToolMetadata {
inputSchema?: Schema;
outputSchema?: Schema;
summary: string;
@@ -30,6 +30,11 @@ interface ToolMetadata {
command: string;
toolName: string;
support: ToolSupportFlags;
+ app?: ToolApp;
+}
+
+export interface ToolApp {
+ resourceUri: string;
}
export interface Schema {
@@ -67,7 +72,17 @@ export type ToolMethod = {
export interface ToolOutput {
status: 'complete' | 'error';
message?: string;
- result: T;
+ result?: T;
+}
+
+export class ToolError extends Error {
+ constructor(
+ message: string,
+ readonly result?: T
+ ) {
+ super(message);
+ this.name = 'ToolError';
+ }
}
/**
@@ -94,7 +109,8 @@ export function tool({
annotations,
inputSchema,
outputSchema,
- support
+ support,
+ app
}: {
summary: string;
description?: string;
@@ -102,6 +118,7 @@ export function tool({
inputSchema?: Schema;
outputSchema?: Schema;
support?: ToolSupportFlags;
+ app?: ToolApp;
}) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return function (originalMethod: ToolMethod, _context: ClassMethodDecoratorContext) {
@@ -119,7 +136,8 @@ export function tool({
outputSchema,
name: originalMethod.name,
title: originalMethod.name.replace(/([A-Z])/g, ' $1').trim(),
- command
+ command,
+ app
};
Object.assign(originalMethod, { metadata });
return originalMethod;
@@ -146,7 +164,11 @@ export function loadTools(obj: any): ManagedToolMethod[] {
try {
return { status: 'complete', message: '', result: await obj[name](...args) };
} catch (e) {
- return { status: 'error', message: (e as Error).message };
+ return {
+ status: 'error',
+ message: e instanceof Error ? e.message : String(e),
+ result: e instanceof ToolError ? e.result : undefined
+ };
}
};
obj[toolName].metadata = obj[name].metadata;
diff --git a/projects/internals/tools/src/playground/service.test.ts b/projects/internals/tools/src/playground/service.test.ts
index a3ec20e80..66e4ea009 100644
--- a/projects/internals/tools/src/playground/service.test.ts
+++ b/projects/internals/tools/src/playground/service.test.ts
@@ -5,7 +5,7 @@ import { writeFileSync, unlinkSync, mkdtempSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
-import type { ToolMethod } from '../internal/tools.js';
+import { loadTools, type ToolMethod, type ToolOutput } from '../internal/tools.js';
import { PlaygroundService } from './service.js';
import { createPlaygroundURL } from './utils.js';
@@ -80,7 +80,7 @@ describe('PlaygroundService', () => {
expect((PlaygroundService.create as ToolMethod).metadata.name).toBe('create');
expect((PlaygroundService.create as ToolMethod).metadata.command).toBe('create');
expect((PlaygroundService.create as ToolMethod).metadata.description).toBe(
- 'Create a shareable playground URL from an HTML template. Returns URL if valid, or validation errors if invalid. Tip: Use playground_validate first to check for issues.'
+ 'Create a shareable playground URL from an HTML template. Returns URL if valid. Lint failures return a tool error. Tip: Use playground_validate first to check for issues.'
);
expect((PlaygroundService.create as ToolMethod).metadata.inputSchema?.properties?.template).toBeDefined();
});
@@ -96,27 +96,56 @@ describe('PlaygroundService', () => {
process.env.ELEMENTS_ENV = originalEnv;
});
- it('should return lint errors when template has validation errors in mcp environment', async () => {
+ it('should reject templates with validation errors in mcp environment', async () => {
process.env.ELEMENTS_ENV = 'mcp';
- const result = await PlaygroundService.create({
- template: 'hello',
- start: false
+ await expect(
+ PlaygroundService.create({
+ template: 'hello',
+ start: false
+ })
+ ).rejects.toMatchObject({
+ message: 'Template validation failed',
+ result: expect.arrayContaining([
+ expect.objectContaining({
+ message: expect.stringContaining('Unexpected use of restricted attribute "nve-layout"')
+ })
+ ])
});
- expect(Array.isArray(result)).toBe(true);
- expect((result as unknown[]).length).toBeGreaterThan(0);
- expect((result as { message: string }[])[0].message).toContain(
- 'Unexpected use of restricted attribute "nve-layout" on . Remove the attribute.'
- );
});
- it('should return lint errors when template has validation errors in cli environment', async () => {
+ it('should reject templates with validation errors in cli environment', async () => {
process.env.ELEMENTS_ENV = 'cli';
- const result = await PlaygroundService.create({
+ await expect(
+ PlaygroundService.create({
+ template: 'hello',
+ start: false
+ })
+ ).rejects.toMatchObject({
+ message: 'Template validation failed',
+ result: expect.arrayContaining([
+ expect.objectContaining({
+ message: expect.stringContaining('Unexpected use of restricted attribute "nve-layout"')
+ })
+ ])
+ });
+ });
+
+ it('should return a managed tool error when create validation fails', async () => {
+ process.env.ELEMENTS_ENV = 'mcp';
+ const tools = loadTools(PlaygroundService);
+ const createTool = tools.find(tool => tool.metadata.name === 'create');
+
+ const result = (await createTool?.({
template: 'hello',
start: false
- });
- expect(Array.isArray(result)).toBe(true);
- expect((result as unknown[]).length).toBeGreaterThan(0);
+ })) as ToolOutput<{ message: string }[]>;
+
+ expect(result.status).toBe('error');
+ expect(result.message).toBe('Template validation failed');
+ expect(result.result).toHaveLength(2);
+ expect(result.result?.[0]?.message).toContain(
+ 'Unexpected use of restricted attribute "nve-layout" on . Remove the attribute.'
+ );
});
it('should skip validation and return URL when not in mcp or cli environment', async () => {
@@ -270,13 +299,11 @@ describe('PlaygroundService', () => {
}
});
- it('create should return lint errors from file path in mcp environment', async () => {
+ it('create should reject lint errors from file path in mcp environment', async () => {
process.env.ELEMENTS_ENV = 'mcp';
writeFileSync(tempFile, 'hello', 'utf8');
- const result = await PlaygroundService.create({ path: tempFile, start: false });
- expect(Array.isArray(result)).toBe(true);
- expect((result as { message: string }[])[0].message).toContain(
- 'Unexpected use of restricted attribute "nve-layout"'
+ await expect(PlaygroundService.create({ path: tempFile, start: false })).rejects.toThrow(
+ 'Template validation failed'
);
});
diff --git a/projects/internals/tools/src/playground/service.ts b/projects/internals/tools/src/playground/service.ts
index 63a5951cc..a857a2842 100644
--- a/projects/internals/tools/src/playground/service.ts
+++ b/projects/internals/tools/src/playground/service.ts
@@ -10,7 +10,7 @@ import {
playgroundTypes,
resolveTemplate
} from './utils.js';
-import { service, tool } from '../internal/tools.js';
+import { service, tool, ToolError } from '../internal/tools.js';
import { ELEMENTS_ENV_ICON } from '../internal/utils.js';
import { eslintSchema } from '../internal/schema.js';
@@ -58,8 +58,8 @@ export class PlaygroundService {
const templateContent = await resolveTemplate({ template, path });
if (process.env.ELEMENTS_ENV === 'mcp' || process.env.ELEMENTS_ENV === 'cli') {
- const { lintPlaygroundTemplate } = await import('@nvidia-elements/lint/eslint/internals');
- return await lintPlaygroundTemplate(templateContent);
+ const { lintTemplate } = await import('@nvidia-elements/lint/eslint/internals');
+ return await lintTemplate(templateContent, { strict: true });
} else {
return [];
}
@@ -68,7 +68,7 @@ export class PlaygroundService {
@tool({
summary: 'Create a shareable playground URL from an HTML template.',
description:
- 'Create a shareable playground URL from an HTML template. Returns URL if valid, or validation errors if invalid. Tip: Use playground_validate first to check for issues.',
+ 'Create a shareable playground URL from an HTML template. Returns URL if valid. Lint failures return a tool error. Tip: Use playground_validate first to check for issues.',
inputSchema: {
type: 'object',
properties: {
@@ -107,7 +107,7 @@ export class PlaygroundService {
{
type: 'array',
items: eslintSchema,
- description: 'Template errors requiring correction.',
+ description: 'Template errors requiring correction when returned with status error.',
additionalProperties: false
}
]
@@ -119,15 +119,15 @@ export class PlaygroundService {
name,
type,
author
- }: PlaygroundOptions & { author?: string }): Promise {
+ }: PlaygroundOptions & { author?: string }): Promise {
const templateContent = await resolveTemplate({ template, path });
if (process.env.ELEMENTS_ENV === 'mcp' || process.env.ELEMENTS_ENV === 'cli') {
- const { lintPlaygroundTemplate } = await import('@nvidia-elements/lint/eslint/internals');
- const lintResult = await lintPlaygroundTemplate(templateContent);
+ const { lintTemplate } = await import('@nvidia-elements/lint/eslint/internals');
+ const lintResult = await lintTemplate(templateContent, { strict: true });
if (lintResult.length > 0) {
- return lintResult;
+ throw new ToolError('Template validation failed', lintResult);
}
}
diff --git a/projects/internals/tools/src/playground/utils.ts b/projects/internals/tools/src/playground/utils.ts
index 2883f8390..153245a24 100644
--- a/projects/internals/tools/src/playground/utils.ts
+++ b/projects/internals/tools/src/playground/utils.ts
@@ -7,8 +7,8 @@ import type { Element } from '@internals/metadata';
import { getElementImports } from '../internal/utils.js';
import { validateTemplate } from '../internal/validate.js';
-declare const __ELEMENTS_PLAYGROUND_BASE_URL__: string;
-declare const __ELEMENTS_ESM_CDN_BASE_URL__: string;
+const ELEMENTS_PLAYGROUND_BASE_URL = process.env.ELEMENTS_PLAYGROUND_BASE_URL ?? '';
+const ELEMENTS_ESM_CDN_BASE_URL = process.env.ELEMENTS_ESM_CDN_BASE_URL ?? '';
interface PlaygroundOptions {
type?: PlaygroundType;
@@ -146,9 +146,9 @@ export function createVueFiles(content: string, elements: Element[], options: Pl
function createURL(files: string, options: PlaygroundOptions) {
const defaultOptions = { openFile: 'index.html', ...options };
- return __ELEMENTS_PLAYGROUND_BASE_URL__?.length > 0
+ return ELEMENTS_PLAYGROUND_BASE_URL.length > 0
? encodeURI(
- `${__ELEMENTS_PLAYGROUND_BASE_URL__}/?version=1&layout=vertical-split${defaultOptions.name ? `&name=${defaultOptions.name.trim()}` : ''}${defaultOptions.theme ? `&theme=${defaultOptions.theme}` : ''}&file=${defaultOptions.openFile}${defaultOptions.referer ? `&ref=${defaultOptions.referer}` : ''}&files=${files}`
+ `${ELEMENTS_PLAYGROUND_BASE_URL}/?version=1&layout=vertical-split${defaultOptions.name ? `&name=${defaultOptions.name.trim()}` : ''}${defaultOptions.theme ? `&theme=${defaultOptions.theme}` : ''}&file=${defaultOptions.openFile}${defaultOptions.referer ? `&ref=${defaultOptions.referer}` : ''}&files=${files}`
)
: '';
}
@@ -254,7 +254,7 @@ const frameworkImportMap: Record Record
};
function createImportMap(framework: 'react' | 'preact' | 'angular' | 'lit' | 'vue' | 'vanilla' = 'vanilla') {
- const CDN_MODULES_URL = __ELEMENTS_ESM_CDN_BASE_URL__;
+ const CDN_MODULES_URL = ELEMENTS_ESM_CDN_BASE_URL;
const imports: Record = {
'@nvidia-elements/core': `${CDN_MODULES_URL}/@nvidia-elements/core@latest`,
'@nvidia-elements/core/': `${CDN_MODULES_URL}/@nvidia-elements/core@latest/`,
diff --git a/projects/lint/src/eslint/internals/index.test.ts b/projects/lint/src/eslint/internals/index.test.ts
index 5d9aaaba3..bdb3ace02 100644
--- a/projects/lint/src/eslint/internals/index.test.ts
+++ b/projects/lint/src/eslint/internals/index.test.ts
@@ -2,26 +2,26 @@
// SPDX-License-Identifier: Apache-2.0
import { describe, it, expect } from 'vitest';
-import { lintPlaygroundTemplate } from './index.js';
+import { lintTemplate } from './index.js';
describe('lintPlaygroundTemplate', () => {
it('should return empty array for valid HTML code', async () => {
const validCode = 'Hello World
';
- const result = await lintPlaygroundTemplate(validCode);
+ const result = await lintTemplate(validCode, { strict: true });
expect(result).toEqual([]);
});
it('should return empty array for valid custom elements', async () => {
const validCode = 'Click me';
- const result = await lintPlaygroundTemplate(validCode);
+ const result = await lintTemplate(validCode, { strict: true });
expect(result).toEqual([]);
});
it('should detect restricted attributes on custom elements', async () => {
const codeWithRestrictedAttribute = 'Button';
- const result = await lintPlaygroundTemplate(codeWithRestrictedAttribute);
+ const result = await lintTemplate(codeWithRestrictedAttribute, { strict: true });
expect(result.length).toBeGreaterThan(0);
expect(
@@ -32,7 +32,7 @@ describe('lintPlaygroundTemplate', () => {
it('should map ESLint severity correctly', async () => {
const codeWithViolation = 'Button';
- const result = await lintPlaygroundTemplate(codeWithViolation);
+ const result = await lintTemplate(codeWithViolation, { strict: true });
expect(result.length).toBeGreaterThan(0);
const message = result[0];
@@ -41,7 +41,7 @@ describe('lintPlaygroundTemplate', () => {
it('should include correct line and column information', async () => {
const codeWithViolation = 'Button';
- const result = await lintPlaygroundTemplate(codeWithViolation);
+ const result = await lintTemplate(codeWithViolation, { strict: true });
expect(result.length).toBeGreaterThan(0);
const message = result[0];
@@ -55,7 +55,7 @@ describe('lintPlaygroundTemplate', () => {
it('should return proper TemplateLintMessage structure', async () => {
const codeWithViolation = 'Button';
- const result = await lintPlaygroundTemplate(codeWithViolation);
+ const result = await lintTemplate(codeWithViolation, { strict: true });
expect(result.length).toBeGreaterThan(0);
const message = result[0];
@@ -80,7 +80,7 @@ describe('lintPlaygroundTemplate', () => {
});
it('should return warning for empty string input', async () => {
- const result = await lintPlaygroundTemplate('');
+ const result = await lintTemplate('', { strict: true });
expect(result).toHaveLength(1);
expect(result[0].id).toBe('empty-template');
expect(result[0].severity).toBe('warn');
@@ -88,14 +88,14 @@ describe('lintPlaygroundTemplate', () => {
});
it('should return warning for whitespace-only input', async () => {
- const result = await lintPlaygroundTemplate(' \n\t ');
+ const result = await lintTemplate(' \n\t ', { strict: true });
expect(result).toHaveLength(1);
expect(result[0].id).toBe('empty-template');
});
it('should handle malformed HTML gracefully', async () => {
const malformedCode = 'Unclosed tags';
- const result = await lintPlaygroundTemplate(malformedCode);
+ const result = await lintTemplate(malformedCode, { strict: true });
expect(Array.isArray(result)).toBe(true);
});
@@ -104,7 +104,7 @@ describe('lintPlaygroundTemplate', () => {
Button 1
Button 2
`;
- const result = await lintPlaygroundTemplate(codeWithMultipleViolations);
+ const result = await lintTemplate(codeWithMultipleViolations, { strict: true });
expect(result.length).toBeGreaterThan(1);
expect(result[0].id).toBe('no-restricted-attributes-with-supported');
@@ -113,7 +113,7 @@ describe('lintPlaygroundTemplate', () => {
it('should handle suggestions', async () => {
const codeWithSuggestion = 'Button';
- const result = await lintPlaygroundTemplate(codeWithSuggestion);
+ const result = await lintTemplate(codeWithSuggestion, { strict: true });
expect(result.length).toBeGreaterThan(0);
const messageWithSuggestion = result.find(msg => msg.id === 'unexpected-deprecated-global-attribute');
@@ -121,4 +121,18 @@ describe('lintPlaygroundTemplate', () => {
expect(messageWithSuggestion.suggestions).toBeDefined();
expect(messageWithSuggestion.suggestions.length).toBeGreaterThan(0);
});
+
+ it('should apply non-strict template rules as warnings', async () => {
+ const result = await lintTemplate('', { strict: false });
+ const tailwindMessage = result.find(message => message.id === 'no-tailwind-class-with-suggestion');
+
+ expect(tailwindMessage?.severity).toBe('warn');
+ });
+
+ it('should apply strict template rules as errors', async () => {
+ const result = await lintTemplate('', { strict: true });
+ const tailwindMessage = result.find(message => message.id === 'no-tailwind-class-with-suggestion');
+
+ expect(tailwindMessage?.severity).toBe('error');
+ });
});
diff --git a/projects/lint/src/eslint/internals/index.ts b/projects/lint/src/eslint/internals/index.ts
index 3c6ea4d62..2360e2a46 100644
--- a/projects/lint/src/eslint/internals/index.ts
+++ b/projects/lint/src/eslint/internals/index.ts
@@ -24,24 +24,26 @@ export interface TemplateLintMessage {
endColumn: number;
}
-export async function lintPlaygroundTemplate(code: string): Promise {
- // extra restrictions/rules as agents rarely use these advanced APIs correctly for playground generation out of context of an established project
- const rules: Partial = {
- '@nvidia-elements/lint/no-unexpected-style-customization': ['error'],
- '@nvidia-elements/lint/no-missing-gap-space': ['error'],
- '@nvidia-elements/lint/no-missing-slotted-elements': ['error', { 'nve-card': { required: ['nve-card-content'] } }],
- '@nvidia-elements/lint/no-unexpected-global-attribute-value': ['error', { distilled: true }],
- '@nvidia-elements/lint/no-tailwind-classes': ['error', { strict: true }]
- };
- return lintString(code, rules);
+interface TemplateLintOptions {
+ strict: boolean;
}
-export async function lintTemplate(code: string): Promise {
- const rules: Partial = {
- '@nvidia-elements/lint/no-unexpected-global-attribute-value': ['error', { distilled: true }],
- '@nvidia-elements/lint/no-tailwind-classes': ['warn', { strict: true }],
- '@nvidia-elements/lint/no-missing-gap-space': ['warn']
- };
+const strictTemplateRules: Partial = {
+ '@nvidia-elements/lint/no-unexpected-global-attribute-value': ['error', { distilled: true }],
+ '@nvidia-elements/lint/no-tailwind-classes': ['error', { strict: true }],
+ '@nvidia-elements/lint/no-unexpected-style-customization': ['error'],
+ '@nvidia-elements/lint/no-missing-gap-space': ['error'],
+ '@nvidia-elements/lint/no-missing-slotted-elements': ['error', { 'nve-card': { required: ['nve-card-content'] } }]
+};
+
+const templateRules: Partial = {
+ '@nvidia-elements/lint/no-unexpected-global-attribute-value': ['error', { distilled: true }],
+ '@nvidia-elements/lint/no-tailwind-classes': ['warn', { strict: true }],
+ '@nvidia-elements/lint/no-missing-gap-space': ['warn']
+};
+
+export async function lintTemplate(code: string, { strict }: TemplateLintOptions): Promise {
+ const rules = strict ? strictTemplateRules : templateRules;
return lintString(code, rules);
}
diff --git a/release.config.js b/release.config.js
index 17171b130..fd5b2f7b8 100644
--- a/release.config.js
+++ b/release.config.js
@@ -94,18 +94,7 @@ export default {
'@semantic-release/github',
{
successComment:
- '🎉 This issue has been resolved in version ${nextRelease.version} 🎉\n\n[Changelog](https://NVIDIA.github.io/elements/docs/changelog/)',
- ...(scope === 'cli'
- ? {
- assets: [
- { path: 'dist/nve-macos-arm64', name: 'nve-macos-arm64', label: 'CLI (macOS ARM64)' },
- { path: 'dist/nve-macos-x64', name: 'nve-macos-x64', label: 'CLI (macOS x64)' },
- { path: 'dist/nve-linux-x64', name: 'nve-linux-x64', label: 'CLI (Linux x64)' },
- { path: 'dist/nve-linux-arm64', name: 'nve-linux-arm64', label: 'CLI (Linux ARM64)' },
- { path: 'dist/nve-windows-x64.exe', name: 'nve-windows-x64.exe', label: 'CLI (Windows x64)' }
- ]
- }
- : {})
+ '🎉 This issue has been resolved in version ${nextRelease.version} 🎉\n\n[Changelog](https://NVIDIA.github.io/elements/docs/changelog/)'
}
]
]