Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<!-- markdownlint-disable MD041 -->
<div align="center">

<img src="assets/social-card.webp" alt="HELiXiR — MCP Server for Web Component Libraries" width="600">

# HELiXiR

**Give AI agents full situational awareness of any web component library.**
Expand All @@ -13,6 +15,9 @@ Stop AI hallucinations. Ground every component suggestion in your actual Custom
[![Node 20+](https://img.shields.io/badge/node-%3E%3D20-brightgreen)](https://nodejs.org)
[![Build](https://img.shields.io/github/actions/workflow/status/bookedsolidtech/helixir/build.yml?branch=main&label=build)](https://github.com/bookedsolidtech/helixir/actions/workflows/build.yml)
[![Tests](https://img.shields.io/github/actions/workflow/status/bookedsolidtech/helixir/test.yml?branch=main&label=tests)](https://github.com/bookedsolidtech/helixir/actions/workflows/test.yml)
[![MCP Protocol](https://img.shields.io/badge/MCP-protocol-purple)](https://modelcontextprotocol.io)
[![TypeScript](https://img.shields.io/badge/TypeScript-strict-blue?logo=typescript)](https://www.typescriptlang.org)
[![Tools](https://img.shields.io/badge/tools-87%2B-purple)](https://www.npmjs.com/package/helixir)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Unify the tool-count messaging.

Line 20 shows tools-87+, but the README copy still says 30+ MCP tools (Line 31). Please make these consistent to avoid confusing users.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` at line 20, The README has inconsistent tool counts: the badge
markdown [![Tools](https://img.shields.io/badge/tools-87%2B-purple)] shows "87+"
while the body text still reads "30+ MCP tools"; update the README so both
references match (e.g., change the body copy "30+ MCP tools" to "87+ MCP tools"
or vice versa) to keep the messaging consistent; locate the badge markup and the
phrase "30+ MCP tools" and make them identical.


[Quick Start](#quick-start) · [Why HELiXiR](#why-helixir) · [Tools Reference](#tools-reference) · [Configuration](#configuration) · [AI Tool Configs](#ai-tool-configs)

Expand Down
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
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",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify manifest vs lockfile specifiers for `@modelcontextprotocol/sdk` without modifying repo.

set -euo pipefail

echo "== package.json entries =="
rg -n '"@modelcontextprotocol/sdk"\s*:' packages/core/package.json packages/mcp/package.json

echo
echo "== lockfile entries (importers + packages) =="
rg -n '@modelcontextprotocol/sdk|packages/core|packages/mcp' pnpm-lock.yaml -C2

echo
echo "Expected: lockfile importer specifiers match '^1.27.1' where corresponding manifests were updated."

Repository: bookedsolidtech/helixir

Length of output: 1454


Update pnpm-lock.yaml to match the dependency bump in both packages (CI blocker).

The manifests for both packages/core (line 19) and packages/mcp (line 49) were updated to ^1.27.1, but the lockfile still specifies ^1.26.0 for both importers. Regenerate and commit pnpm-lock.yaml so frozen-lockfile installs work correctly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/package.json` at line 19, The pnpm lockfile is out of sync with
the bumped dependency "@modelcontextprotocol/sdk" in package manifests; update
the lockfile by running a lockfile regeneration (e.g., pnpm install or pnpm i)
at the repo root so pnpm-lock.yaml reflects the new version ^1.27.1 used in
packages/core and packages/mcp, then commit the updated pnpm-lock.yaml so CI's
frozen-lockfile installs succeed.

"zod": "^3.22.0"
},
"peerDependencies": {
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})`;
}
Comment on lines 147 to 149
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In CSS Custom Properties, what is the computed behavior of a self-referential declaration like --x: var(--x);? Is it treated as invalid due to a cycle?

💡 Result:

In CSS Custom Properties, a self-referential declaration like --x: var(--x); creates a cyclic dependency. The browser constructs a dependency graph for custom properties on each element. If a custom property's value contains a var reference to itself (directly or indirectly through other properties), it forms a cycle. All custom properties in the cycle are invalid at computed-value time, resulting in the "guaranteed-invalid value" (initial value for custom properties). When var(--x) is used elsewhere, it substitutes nothing (empty token stream), and fallbacks (if provided) are used. This behavior is consistent across browsers and prevents infinite recursion during resolution.

Citations:


🏁 Script executed:

cat -n packages/core/src/handlers/theme.ts | sed -n '140,160p'

Repository: bookedsolidtech/helixir

Length of output: 870


🏁 Script executed:

# Find the test file mentioned in the scratchpad
fd "theme.test" -t f

Repository: bookedsolidtech/helixir

Length of output: 95


🏁 Script executed:

# Check the full context of the lightPlaceholder function
rg -A 10 -B 10 "lightPlaceholder" packages/core/src/handlers/theme.ts

Repository: bookedsolidtech/helixir

Length of output: 1596


🏁 Script executed:

# Find and examine the test file
fd "theme.test.ts" -t f -x wc -l {}

Repository: bookedsolidtech/helixir

Length of output: 101


🏁 Script executed:

# Look at tests related to default/unknown categories
rg -A 20 "unknown|default" tests/handlers/theme.test.ts

Repository: bookedsolidtech/helixir

Length of output: 1914


🏁 Script executed:

# Show how lightValues are used in the createTheme function
rg -A 10 "lightValues" packages/core/src/handlers/theme.ts

Repository: bookedsolidtech/helixir

Length of output: 1494


Self-referential fallback creates invalid custom-property values

Line 148 generates --x: var(--x); for unknown categories, which creates a CSS custom-property cycle. Per the W3C CSS Variables specification, cyclic custom properties are treated as invalid at computed-value time, making unknown tokens effectively unusable in generated CSS.

Proposed fix
    default:
-      return `var(${tokenName})`;
+      return 'initial';

Please also adjust the test in tests/handlers/theme.test.ts (line ~105: produces a var() fallback for tokens with unknown categories) to assert a non-cyclic fallback value instead of var(--my-custom-widget).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/handlers/theme.ts` around lines 147 - 149, The default
branch in the theme handler currently returns a self-referential CSS variable
(`return var(${tokenName})`) which produces cycles like `--x: var(--x);`; change
the default to return a non-cyclic fallback, e.g. `var(${tokenName}, unset)` (or
another concrete non-custom fallback value you prefer) inside the function that
returns token CSS values (the code path using tokenName in
packages/core/src/handlers/theme.ts), and update the test in
tests/handlers/theme.test.ts (the "produces a var() fallback for tokens with
unknown categories" assertion) to expect the new non-cyclic fallback (`unset` or
your chosen fallback) instead of `var(--my-custom-widget)`.

}

Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/tools/library.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { z } from 'zod';
import { readFileSync } from 'fs';
import { readFile } from 'fs/promises';
import { resolve, sep } from 'path';

import type { McpWcConfig } from '../config.js';
Expand Down Expand Up @@ -124,7 +124,7 @@ export async function handleLibraryCall(
}
let raw: string;
try {
raw = readFileSync(absPath, 'utf-8');
raw = await readFile(absPath, 'utf-8');
} catch {
return createErrorResponse(`CEM file not found at ${absPath}`);
}
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);
});
67 changes: 67 additions & 0 deletions packages/vscode/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
{
"name": "helixir-vscode",
"displayName": "Helixir",
"description": "AI-powered web component intelligence for VS Code — powered by helixir MCP",
"version": "0.1.0",
"publisher": "bookedsolidtech",
"private": true,
"engines": {
"vscode": "^1.99.0"
},
"categories": [
"Other",
"AI"
],
"keywords": [
"mcp",
"web components",
"helixir",
"ai",
"custom elements"
],
"activationEvents": [
"onStartupFinished"
],
"main": "./dist/extension.js",
"contributes": {
"mcpServerDefinitionProviders": [
{
"id": "helixir",
"label": "Helixir"
}
],
"commands": [
{
"command": "helixir.runHealthCheck",
"title": "Helixir: Run Health Check",
"category": "Helixir"
}
],
"configuration": {
"title": "Helixir",
"properties": {
"helixir.configPath": {
"type": "string",
"default": "",
"description": "Path to mcpwc.config.json (relative to workspace root or absolute). Leave empty to use workspace root defaults."
}
}
}
},
"scripts": {
"vscode:prepublish": "node esbuild.config.mjs --production",
"build": "node esbuild.config.mjs",
"watch": "node esbuild.config.mjs --watch",
"package": "vsce package --no-dependencies",
"publish": "vsce publish --no-dependencies"
},
"dependencies": {
"helixir": "workspace:*"
},
"devDependencies": {
"@types/vscode": "^1.99.0",
"@vscode/vsce": "^3.0.0",
"esbuild": "^0.25.0",
"ovsx": "^0.9.0"
}
}
39 changes: 39 additions & 0 deletions packages/vscode/src/extension.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import * as vscode from 'vscode';
import { registerMcpProvider } from './mcpProvider.js';

/**
* Called when the extension is activated.
* Registers the Helixir MCP server definition provider and the
* "Helixir: Run Health Check" command.
*/
export function activate(context: vscode.ExtensionContext): void {
registerMcpProvider(context);

const healthCheckCommand = vscode.commands.registerCommand(
'helixir.runHealthCheck',
async () => {
const workspaceFolders = vscode.workspace.workspaceFolders;
if (!workspaceFolders || workspaceFolders.length === 0) {
await vscode.window.showErrorMessage(
'Helixir: No workspace folder is open. ' +
'Open a component library folder to run a health check.'
);
return;
}

await vscode.window.showInformationMessage(
'Helixir: MCP server is active. ' +
'Ask your AI assistant to call score_all_components via the Helixir MCP server.'
);
}
);

context.subscriptions.push(healthCheckCommand);
}

/**
* Called when the extension is deactivated.
*/
export function deactivate(): void {
// Subscriptions are disposed automatically via context.subscriptions.
}
16 changes: 16 additions & 0 deletions packages/vscode/src/mcp-server-entry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* Helixir MCP Server — entry point for the bundled server.
*
* This file is bundled by esbuild into dist/mcp-server.js (ESM format).
* It is spawned as a child process by the VS Code extension (mcpProvider.ts)
* using stdio transport.
*
* The helixir/mcp module exports a `main()` function that initialises and
* starts the MCP server, listening on stdin/stdout.
*/
import { main } from 'helixir/mcp';

main().catch((err: unknown) => {
process.stderr.write(`[helixir-mcp] Fatal: ${String(err)}\n`);
process.exit(1);
});
Loading
Loading