Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
8e131fe
docs: add social card image + update README badges
himerus Mar 26, 2026
ba6ec2b
docs: update social card with color-branded export
himerus Mar 26, 2026
933fa0a
refactor: fix: align @modelcontextprotocol/sdk versions across monore…
himerus Mar 26, 2026
311f1c4
Merge pull request #206 from bookedsolidtech/feature/fix-align-modelc…
himerus Mar 26, 2026
f94ea7b
refactor: fix: wire scaffold_component and extend_component into MCP …
himerus Mar 26, 2026
96cb5e2
Merge pull request #207 from bookedsolidtech/feature/fix-wire-scaffol…
himerus Mar 26, 2026
692886b
feat: scaffold packages/vscode VS Code extension MVP
himerus Mar 26, 2026
0d24d51
Merge pull request #208 from bookedsolidtech/feature/feat-scaffold-pa…
himerus Mar 26, 2026
a7e99c0
docs: update README social card to PNG and fix tool count to 87+
himerus Mar 26, 2026
02ee086
fix: replace TODO placeholder with var() fallback in theme handler de…
himerus Mar 26, 2026
fd40ef2
test: add test suite for styling tools (29 tools, 75 tests)
himerus Mar 26, 2026
cd70600
Merge pull request #210 from bookedsolidtech/feature/fix-theme-handle…
himerus Mar 26, 2026
41f5237
Merge remote-tracking branch 'origin/dev' into feature/docs-update-re…
himerus Mar 26, 2026
e9e4a58
Merge pull request #211 from bookedsolidtech/feature/test-add-test-su…
himerus Mar 26, 2026
42524fc
fix: replace plain new Error() throws with MCPError
himerus Mar 26, 2026
d5d6552
Merge pull request #214 from bookedsolidtech/feature/fix-replace-plai…
himerus Mar 26, 2026
7c05077
fix: update README tool count badge and add missing tools to reference
himerus Mar 26, 2026
97a3edb
Merge pull request #215 from bookedsolidtech/feature/fix-update-readm…
himerus Mar 26, 2026
d695d62
fix: add typecheck script alias to resolve post-merge verification fa…
himerus Mar 26, 2026
8b8d40b
Merge pull request #216 from bookedsolidtech/feature/fix-post-merge-v…
himerus Mar 26, 2026
51905e0
test: add test suites for scaffold, extend, theme, and bundle tools
himerus Mar 26, 2026
1909566
Merge pull request #217 from bookedsolidtech/feature/test-add-test-su…
himerus Mar 26, 2026
60ad017
Merge branch 'origin/dev' into feature/docs-update-readme-social-card-to
himerus Mar 26, 2026
fa8b4c6
Merge pull request #209 from bookedsolidtech/feature/docs-update-read…
himerus Mar 26, 2026
0508d52
test: add test suites for cdn, composition, framework, story, tokens,…
himerus Mar 26, 2026
37eca94
Merge pull request #218 from bookedsolidtech/feature/test-add-test-su…
himerus Mar 26, 2026
2bbbfb7
refactor: replace env var if-blocks with lookup tables in config.ts
himerus Mar 26, 2026
496dede
Merge pull request #219 from bookedsolidtech/feature/fix-refactor-con…
himerus Mar 26, 2026
1436936
feat(vscode): add Configure for Cursor/Windsurf command
himerus Mar 26, 2026
8208b22
Merge pull request #220 from bookedsolidtech/feature/feat-add-cursorw…
himerus Mar 26, 2026
201ebaa
fix: add bounds checking for CLI array args before access
himerus Mar 26, 2026
600373c
Merge pull request #221 from bookedsolidtech/feature/fix-add-bounds-c…
himerus Mar 26, 2026
e62ea1f
test: add test suites for 8 untested analyzer modules
himerus Mar 26, 2026
19c25c4
fix: correct test assertions to match actual analyzer APIs
himerus Mar 26, 2026
db217f2
Merge pull request #222 from bookedsolidtech/feature/test-add-test-su…
himerus Mar 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 76 additions & 9 deletions README.md

Large diffs are not rendered by default.

Binary file added assets/social-card.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/social-card.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions build
1 change: 1 addition & 0 deletions node_modules
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"test:coverage": "vitest run --coverage",
"test:watch": "vitest",
"type-check": "tsc --noEmit",
"typecheck": "pnpm run type-check",
"lint": "eslint src packages/core/src",
"lint:fix": "eslint src packages/core/src --fix",
"format": "prettier --write .",
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
},
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.26.0",
"@modelcontextprotocol/sdk": "^1.27.1",
"zod": "^3.22.0"
},
"peerDependencies": {
Expand Down
70 changes: 25 additions & 45 deletions packages/core/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ const defaults: McpWcConfig = {
};

function readConfigFile(projectRoot: string): Partial<McpWcConfig> {
// Primary config file name
const primaryPath = resolve(projectRoot, 'helixir.mcp.json');
if (existsSync(primaryPath)) {
try {
Expand All @@ -44,21 +43,6 @@ function readConfigFile(projectRoot: string): Partial<McpWcConfig> {
}
}

// Backward-compatible fallback to legacy config file name
const legacyPath = resolve(projectRoot, 'mcpwc.config.json');
if (existsSync(legacyPath)) {
process.stderr.write(
`[helixir] Warning: mcpwc.config.json is deprecated. Rename to helixir.mcp.json.\n`,
);
try {
const raw = readFileSync(legacyPath, 'utf-8');
return JSON.parse(raw) as Partial<McpWcConfig>;
} catch {
process.stderr.write(`[helixir] Warning: mcpwc.config.json is malformed. Using defaults.\n`);
return {};
}
}

return {};
}

Expand Down Expand Up @@ -92,36 +76,32 @@ export function loadConfig(): Readonly<McpWcConfig> {
}

// Apply env vars (highest priority)
if (process.env['MCP_WC_CEM_PATH'] !== undefined) {
config.cemPath = process.env['MCP_WC_CEM_PATH'];
}
if (process.env['MCP_WC_PROJECT_ROOT'] !== undefined) {
config.projectRoot = process.env['MCP_WC_PROJECT_ROOT'];
}
if (process.env['MCP_WC_COMPONENT_PREFIX'] !== undefined) {
config.componentPrefix = process.env['MCP_WC_COMPONENT_PREFIX'];
}
if (process.env['MCP_WC_HEALTH_HISTORY_DIR'] !== undefined) {
config.healthHistoryDir = process.env['MCP_WC_HEALTH_HISTORY_DIR'];
}
if (process.env['MCP_WC_TSCONFIG_PATH'] !== undefined) {
config.tsconfigPath = process.env['MCP_WC_TSCONFIG_PATH'];
}
if (process.env['MCP_WC_TOKENS_PATH'] !== undefined) {
const val = process.env['MCP_WC_TOKENS_PATH'];
config.tokensPath = val === 'null' ? null : val;
}
if (process.env['MCP_WC_CDN_BASE'] !== undefined) {
const val = process.env['MCP_WC_CDN_BASE'];
config.cdnBase = val === 'null' ? null : val;
}
if (process.env['MCP_WC_CDN_AUTOLOADER'] !== undefined) {
const val = process.env['MCP_WC_CDN_AUTOLOADER'];
config.cdnAutoloader = val === 'null' ? null : val;
// String keys map directly; nullable keys treat the literal string 'null' as null.
const ENV_MAP_STRING: Readonly<Record<string, keyof McpWcConfigMutable>> = {
MCP_WC_CEM_PATH: 'cemPath',
MCP_WC_PROJECT_ROOT: 'projectRoot',
MCP_WC_COMPONENT_PREFIX: 'componentPrefix',
MCP_WC_HEALTH_HISTORY_DIR: 'healthHistoryDir',
MCP_WC_TSCONFIG_PATH: 'tsconfigPath',
};
const ENV_MAP_NULLABLE: Readonly<Record<string, keyof McpWcConfigMutable>> = {
MCP_WC_TOKENS_PATH: 'tokensPath',
MCP_WC_CDN_BASE: 'cdnBase',
MCP_WC_CDN_AUTOLOADER: 'cdnAutoloader',
MCP_WC_CDN_STYLESHEET: 'cdnStylesheet',
};

for (const [envKey, configKey] of Object.entries(ENV_MAP_STRING)) {
const val = process.env[envKey];
if (val !== undefined) {
(config as Record<string, unknown>)[configKey] = val;
}
}
if (process.env['MCP_WC_CDN_STYLESHEET'] !== undefined) {
const val = process.env['MCP_WC_CDN_STYLESHEET'];
config.cdnStylesheet = val === 'null' ? null : val;
for (const [envKey, configKey] of Object.entries(ENV_MAP_NULLABLE)) {
const val = process.env[envKey];
if (val !== undefined) {
(config as Record<string, unknown>)[configKey] = val === 'null' ? null : val;
}
}

// --watch CLI flag overrides config file value
Expand Down
5 changes: 4 additions & 1 deletion packages/core/src/handlers/cem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -725,7 +725,10 @@ export function findComponentsByToken(
cem: Cem,
): FindComponentsByTokenResult {
if (!token.startsWith('--')) {
throw new Error(`CSS custom property name must start with "--": "${token}"`);
throw new MCPError(
`CSS custom property name must start with "--": "${token}"`,
ErrorCategory.VALIDATION,
);
}

const components: TokenComponentMatch[] = [];
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/handlers/dependencies.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Cem } from './cem.js';
import { MCPError, ErrorCategory } from '../shared/error-handling.js';

export interface ComponentDependencyResult {
tagName: string;
Expand Down Expand Up @@ -105,7 +106,7 @@ export function getComponentDependencies(
}

if (!found) {
throw new Error(`Component "${tagName}" not found in CEM.`);
throw new MCPError(`Component "${tagName}" not found in CEM.`, ErrorCategory.NOT_FOUND);
}

const depMap = buildDependencyMap(cem);
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/handlers/extend.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Cem, CemDeclaration } from './cem.js';
import { MCPError, ErrorCategory } from '../shared/error-handling.js';

// --- Helpers ---

Expand Down Expand Up @@ -66,7 +67,7 @@ export function extendComponent(
): ExtendComponentResult {
const parentDecl = findDeclaration(cem, parentTagName);
if (!parentDecl) {
throw new Error(`Component "${parentTagName}" not found in CEM.`);
throw new MCPError(`Component "${parentTagName}" not found in CEM.`, ErrorCategory.NOT_FOUND);
}

const parentClassName = tagNameToClassName(parentTagName);
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/handlers/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ function lightPlaceholder(tokenName: string, category: string): string {
return '200ms';

default:
return '/* TODO: set value */';
return `var(${tokenName})`;
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/mcp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"homepage": "https://github.com/bookedsolidtech/helixir/tree/main/packages/mcp#readme",
"peerDependencies": {
"helixir": ">=0.5.0",
"@modelcontextprotocol/sdk": "^1.26.0",
"@modelcontextprotocol/sdk": "^1.27.1",
"zod": "^3.22.0"
},
"devDependencies": {
Expand Down
30 changes: 30 additions & 0 deletions packages/vscode/.vscodeignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Source files — not needed in the packaged extension
src/
tsconfig.json
esbuild.config.mjs

# Development dependencies and lock files
node_modules/
.pnpm-store/
pnpm-lock.yaml
package-lock.json

# Test artefacts
coverage/
*.test.ts
*.spec.ts

# Build intermediates (keep dist/)
*.map

# Editor and OS artefacts
.vscode/
.DS_Store
*.log

# Root-level workspace files that should not be bundled
../../node_modules/
../../src/
../../build/
../../packages/
../../.github/
84 changes: 84 additions & 0 deletions packages/vscode/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Helixir — VS Code Extension

**AI-powered web component intelligence for VS Code.**

Helixir gives AI assistants full situational awareness of any web component library by wiring the [helixir MCP server](https://github.com/bookedsolidtech/helixir) directly into VS Code's MCP layer.

## Features

- **MCP server auto-registration** — the helixir MCP server starts automatically with VS Code, no manual configuration required
- **30+ MCP tools** — component discovery, health scoring, breaking-change detection, TypeScript diagnostics, design token lookup, and more
- **Zero hallucinations** — every AI component suggestion is grounded in your actual `custom-elements.json`
- **Framework-agnostic** — works with Lit, Stencil, FAST, Spectrum, Shoelace, or any library that produces a Custom Elements Manifest

## Requirements

- VS Code **≥ 1.99.0**
- A component library with a `custom-elements.json` (Custom Elements Manifest)
- Node.js **≥ 20** on `PATH`

## Getting Started

1. Install the extension from the VS Code Marketplace
2. Open your component library folder in VS Code
3. The Helixir MCP server will register automatically with AI assistants that support MCP (e.g., GitHub Copilot, Claude)

### Optional: Configure the Config Path

If your `mcpwc.config.json` is not at the workspace root, set the path via VS Code settings:

```json
// .vscode/settings.json
{
"helixir.configPath": "packages/web-components/mcpwc.config.json"
}
```

The path can be relative to the workspace root or absolute.

## Commands

| Command | Description |
|---------|-------------|
| `Helixir: Run Health Check` | Guides you to run a health check via your AI assistant |

## Extension Settings

| Setting | Type | Default | Description |
|---------|------|---------|-------------|
| `helixir.configPath` | `string` | `""` | Path to `mcpwc.config.json`. Empty = workspace root. |

## How It Works

When the extension activates, it registers a **MCP server definition provider** (`helixir`) with VS Code's language model API (`vscode.lm`). VS Code spawns the bundled helixir MCP server (`dist/mcp-server.js`) as a child process over stdio.

The server reads your `custom-elements.json` and exposes 30+ tools that AI models can call to look up component APIs, run health scans, generate type declarations, and more.

## Configuration Reference

The helixir server is configured via environment variables passed by the extension:

| Variable | Description |
|----------|-------------|
| `MCP_WC_PROJECT_ROOT` | Set to your workspace folder automatically |
| `MCP_WC_CONFIG_PATH` | Set when `helixir.configPath` is configured |

Additional configuration (token path, component prefix, health history dir) belongs in `mcpwc.config.json`. See the [helixir documentation](https://github.com/bookedsolidtech/helixir) for the full config reference.

## Troubleshooting

**MCP server not appearing in AI assistant tools**
- Verify VS Code ≥ 1.99.0 is installed
- Confirm your workspace contains a `custom-elements.json`
- Check the Output panel → Helixir for error messages

**"No workspace folder" error from Run Health Check**
- Open a folder (not just a file) in VS Code — the extension uses the workspace folder as the project root

**Server starts but returns no components**
- Ensure `custom-elements.json` exists at the workspace root or configure `helixir.configPath`
- Regenerate the manifest: `npm run analyze:cem` (or your CEM generation script)

## License

MIT — see [LICENSE](../../LICENSE)
69 changes: 69 additions & 0 deletions packages/vscode/esbuild.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* esbuild configuration for the Helixir VS Code extension.
*
* Produces two bundles:
* dist/extension.js — VS Code extension host entry (CJS, externalizes 'vscode')
* dist/mcp-server.js — Helixir MCP server entry (ESM, bundles helixir)
*/

import * as esbuild from 'esbuild';

const isProduction = process.argv.includes('--production');
const isWatch = process.argv.includes('--watch');

const sharedOptions = {
bundle: true,
sourcemap: !isProduction,
minify: isProduction,
logLevel: 'info',
platform: 'node',
target: 'node20',
};

/**
* Bundle 1: VS Code extension host entry
* - CommonJS (VS Code extension host requires CJS)
* - 'vscode' is externalized — provided by the VS Code runtime
*/
const extensionConfig = {
...sharedOptions,
entryPoints: ['src/extension.ts'],
outfile: 'dist/extension.js',
format: 'cjs',
external: ['vscode'],
};

/**
* Bundle 2: Helixir MCP server
* - ESM format (helixir is an ES module)
* - Bundles helixir and its dependencies so the extension is self-contained
* - Spawned as a child process via stdio by the VS Code extension
*/
const mcpServerConfig = {
...sharedOptions,
entryPoints: ['src/mcp-server-entry.ts'],
outfile: 'dist/mcp-server.js',
format: 'esm',
banner: {
js: '#!/usr/bin/env node\n// Helixir MCP Server — bundled by esbuild',
},
};

async function build() {
const extensionCtx = await esbuild.context(extensionConfig);
const mcpServerCtx = await esbuild.context(mcpServerConfig);

if (isWatch) {
await Promise.all([extensionCtx.watch(), mcpServerCtx.watch()]);
console.log('[helixir-vscode] Watching for changes...');
} else {
await Promise.all([extensionCtx.rebuild(), mcpServerCtx.rebuild()]);
await Promise.all([extensionCtx.dispose(), mcpServerCtx.dispose()]);
console.log('[helixir-vscode] Build complete.');
}
}

build().catch((err) => {
console.error('[helixir-vscode] Build failed:', err);
process.exit(1);
});
Loading
Loading