Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
f7637a0
feat(shared): add detail level enums and optional text/css fields to
ashtonchew Sep 27, 2025
4860ac2
feat(extension): capture text variants and full computed styles in el…
ashtonchew Sep 27, 2025
3254237
feat(server): allow textDetail/cssLevel request parameters and prune …
ashtonchew Sep 27, 2025
041a2ba
feat(server): add element shaping utility for dynamic detail control
ashtonchew Sep 27, 2025
de8f249
feat(server): cover element detail normalization and shaping
ashtonchew Sep 27, 2025
cf3fc32
docs(readme): document optional MCP tool arguments for context control
ashtonchew Sep 27, 2025
2e468ab
feat(changeset): update version with minor change patch
ashtonchew Sep 27, 2025
7cf0678
fix(extension): use LEGACY_ELEMENT_SELECTED enum value
ashtonchew Oct 1, 2025
b5b1d29
feat(server): adapt dynamic context control to ProcessedPointedDOMEle…
ashtonchew Oct 1, 2025
cbb4704
feat(server): store full CSS properties in ProcessedPointedDOMElement
ashtonchew Oct 1, 2025
4ce493e
refactor(server): remove LEGACY_ELEMENT_SELECTED support and dead code
ashtonchew Oct 1, 2025
ca7a516
docs: add v0.6.0 changeset and update architecture documentation
ashtonchew Oct 1, 2025
4fb84d0
fix(extension): return true from message listener to keep channel open
ashtonchew Oct 2, 2025
db24adb
refactor(extension): remove legacy element serializer
ashtonchew Nov 7, 2025
d74cd48
feat(shared): switch detail levels to enums
ashtonchew Nov 7, 2025
08339fb
refactor(server): simplify shared state and css payload
ashtonchew Nov 7, 2025
2faf088
docs(readme): clarify numeric detail levels
ashtonchew Nov 7, 2025
3c48015
Fix lint
elieteyssedou Nov 12, 2025
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
14 changes: 14 additions & 0 deletions .changeset/remove-legacy-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
"@mcp-pointer/server": minor
"@mcp-pointer/shared": minor
---

**Architecture Cleanup & Improvements**

- **Server**: Store full CSS properties in `cssProperties` instead of filtering to 5 properties
- **Server**: Remove LEGACY_ELEMENT_SELECTED support - only DOM_ELEMENT_POINTED is now supported
- **Server**: Delete unused files (`mcp-handler.ts`, `websocket-server.ts`)
- **Server**: Simplify types - remove StateDataV1 and LegacySharedState
- **Server**: Dynamic CSS filtering now happens on-the-fly during MCP tool calls based on cssLevel parameter

This enables full CSS details to be accessible without re-pointing to elements, with filtering applied server-side based on tool parameters.
42 changes: 30 additions & 12 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,16 @@ packages/
├── server/ # @mcp-pointer/server - MCP Server (TypeScript)
│ ├── src/
│ │ ├── start.ts # Main server entry point
│ │ ├── cli.ts # Command line interface
│ │ ├── websocket-server.ts
│ │ └── mcp-handler.ts
│ │ ├── cli.ts # Command line interface
│ │ ├── message-handler.ts # Message routing & state building
│ │ ├── services/
│ │ │ ├── websocket-service.ts # WebSocket with leader election
│ │ │ ├── mcp-service.ts # MCP protocol handler
│ │ │ ├── element-processor.ts # Raw→Processed conversion
│ │ │ └── shared-state-service.ts # State persistence
│ │ └── utils/
│ │ ├── dom-extractor.ts # HTML parsing utilities
│ │ └── element-detail.ts # Dynamic CSS/text filtering
│ ├── dist/
│ │ └── cli.cjs # Bundled standalone CLI
│ └── package.json
Expand All @@ -73,15 +80,17 @@ packages/
│ ├── src/
│ │ ├── background.ts # Service worker
│ │ ├── content.ts # Element selection
│ │ └── element-sender-service.ts
│ │ └── services/
│ │ └── element-sender-service.ts # WebSocket client
│ ├── dev/ # Development build (with logging)
│ ├── dist/ # Production build (minified)
│ └── manifest.json
└── shared/ # @mcp-pointer/shared - Shared TypeScript types
├── src/
│ ├── Logger.ts
│ └── types.ts
│ ├── logger.ts
│ ├── types.ts
│ └── detail.ts # CSS/text detail level constants
└── package.json
```

Expand Down Expand Up @@ -119,9 +128,16 @@ packages/
├── server/ # @mcp-pointer/server - MCP Server (TypeScript)
│ ├── src/
│ │ ├── start.ts # Main server entry point
│ │ ├── cli.ts # Command line interface
│ │ ├── websocket-server.ts
│ │ └── mcp-handler.ts
│ │ ├── cli.ts # Command line interface
│ │ ├── message-handler.ts # Message routing & state building
│ │ ├── services/
│ │ │ ├── websocket-service.ts # WebSocket with leader election
│ │ │ ├── mcp-service.ts # MCP protocol handler
│ │ │ ├── element-processor.ts # Raw→Processed conversion
│ │ │ └── shared-state-service.ts # State persistence
│ │ └── utils/
│ │ ├── dom-extractor.ts # HTML parsing utilities
│ │ └── element-detail.ts # Dynamic CSS/text filtering
│ ├── dist/
│ │ └── cli.cjs # Bundled standalone CLI
│ └── package.json
Expand All @@ -130,15 +146,17 @@ packages/
│ ├── src/
│ │ ├── background.ts # Service worker
│ │ ├── content.ts # Element selection
│ │ └── element-sender-service.ts
│ │ └── services/
│ │ └── element-sender-service.ts # WebSocket client
│ ├── dev/ # Development build (with logging)
│ ├── dist/ # Production build (minified)
│ └── manifest.json
└── shared/ # @mcp-pointer/shared - Shared TypeScript types
├── src/
│ ├── Logger.ts
│ └── types.ts
│ ├── logger.ts
│ ├── types.ts
│ └── detail.ts # CSS/text detail level constants
└── package.json
```

Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ The extension lets you visually select DOM elements in the browser, and the MCP

- 🎯 **`Option+Click` Selection** - Simply hold `Option` (Alt on Windows) and click any element
- 📋 **Complete Element Data** - Text content, CSS classes, HTML attributes, positioning, and styling
- 💡 **Dynamic Context Control** - Request visible-only text, suppress text entirely, or dial CSS detail from none → full computed styles per MCP call
- ⚛️ **React Component Detection** - Component names and source files via Fiber (experimental)
- 🔗 **WebSocket Connection** - Real-time communication between browser and AI tools
- 🤖 **MCP Compatible** - Works with Claude Code and other MCP-enabled AI tools
Expand Down Expand Up @@ -102,7 +103,9 @@ After configuration, **restart your coding tool** to load the MCP connection.
Your AI tool will automatically start the MCP server when needed using the `npx -y @mcp-pointer/server@latest start` command.

**Available MCP Tool:**
- `get-pointed-element` - Get textual information about the currently pointed DOM element from the browser extension
- `get-pointed-element` – Returns textual information about the currently pointed DOM element. Optional arguments:
- `textDetail`: `"full" | "visible" | "none"` (default `"full"`) controls how much text to include.
- `cssLevel`: `0 | 1 | 2 | 3` (default `1`) controls styling detail, from no CSS (0) up to full computed styles (3).

## 🎯 How It Works

Expand Down
4 changes: 4 additions & 0 deletions packages/chrome-extension/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@

## 0.5.0

### Minor Changes

- Added dynamic context control (text detail & css levels)

### Patch Changes

- Updated dependencies [d91e764]
Expand Down
1 change: 1 addition & 0 deletions packages/chrome-extension/src/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ chrome.runtime.onMessage
);

sendResponse({ success: true });
return true; // Keep message channel open for async response
}
});

Expand Down
160 changes: 141 additions & 19 deletions packages/chrome-extension/src/utils/element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,18 @@
/* eslint-disable no-underscore-dangle */

import {
ComponentInfo, CSSProperties, ElementPosition, TargetedElement, RawPointedDOMElement,
ComponentInfo,
CSSDetailLevel,
CSSProperties,
DEFAULT_CSS_LEVEL,
DEFAULT_TEXT_DETAIL,
ElementPosition,
TargetedElement,
TextDetailLevel,
TextSnapshots,
RawPointedDOMElement,
} from '@mcp-pointer/shared/types';
import { CSS_LEVEL_FIELD_MAP } from '@mcp-pointer/shared/detail';
import logger from './logger';

export interface ReactSourceInfo {
Expand All @@ -12,6 +22,105 @@ export interface ReactSourceInfo {
columnNumber?: number;
}

export interface ElementSerializationOptions {
textDetail?: TextDetailLevel;
cssLevel?: CSSDetailLevel;
}

function toKebabCase(property: string): string {
return property
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
.replace(/_/g, '-')
.toLowerCase();
}

function toCamelCase(property: string): string {
return property
.replace(/^-+/, '')
.replace(/-([a-z])/g, (_, char: string) => char.toUpperCase());
}

function getStyleValue(style: CSSStyleDeclaration, property: string): string | undefined {
const camelValue = (style as any)[property];
if (typeof camelValue === 'string' && camelValue.trim().length > 0) {
return camelValue;
}

const kebab = toKebabCase(property);
const value = style.getPropertyValue(kebab);
if (typeof value === 'string' && value.trim().length > 0) {
return value;
}

return undefined;
}

function extractFullCSSProperties(style: CSSStyleDeclaration): Record<string, string> {
const properties: Record<string, string> = {};

for (let i = 0; i < style.length; i += 1) {
const property = style.item(i);

if (property && !property.startsWith('-')) {
const value = style.getPropertyValue(property);
if (typeof value === 'string' && value.trim().length > 0) {
const camel = toCamelCase(property);
properties[camel] = value;
}
}
}

return properties;
}

function getElementCSSProperties(
style: CSSStyleDeclaration,
cssLevel: CSSDetailLevel,
fullCSS: Record<string, string>,
): CSSProperties | undefined {
if (cssLevel === 0) {
return undefined;
}

if (cssLevel === 3) {
return fullCSS;
}

const fields = CSS_LEVEL_FIELD_MAP[cssLevel];
const properties: CSSProperties = {};

fields.forEach((property) => {
const value = getStyleValue(style, property);
if (value !== undefined) {
properties[property] = value;
}
});

return properties;
}

function collectTextVariants(element: HTMLElement): TextSnapshots {
const visible = element.innerText || '';
const full = element.textContent || visible;

return {
visible,
full,
};
}

function resolveTextByDetail(variants: TextSnapshots, detail: TextDetailLevel): string | undefined {
if (detail === 'none') {
return undefined;
}

if (detail === 'visible') {
return variants.visible;
}

return variants.full || variants.visible;
}

/**
* Get source file information from a DOM element's React component
*/
Expand Down Expand Up @@ -172,20 +281,6 @@ export function getElementPosition(element: HTMLElement): ElementPosition {
};
}

/**
* Extract relevant CSS properties from an element
*/
export function getElementCSSProperties(element: HTMLElement): CSSProperties {
const computedStyle = window.getComputedStyle(element);
return {
display: computedStyle.display,
position: computedStyle.position,
fontSize: computedStyle.fontSize,
color: computedStyle.color,
backgroundColor: computedStyle.backgroundColor,
};
}

/**
* Extract CSS classes from an element as an array
*/
Expand All @@ -197,20 +292,47 @@ export function getElementClasses(element: HTMLElement): string[] {
return classNameStr.split(' ').filter((c: string) => c.trim());
}

export function adaptTargetToElement(element: HTMLElement): TargetedElement {
return {
export function adaptTargetToElement(
element: HTMLElement,
options: ElementSerializationOptions = {},
): TargetedElement {
const textDetail = options.textDetail ?? DEFAULT_TEXT_DETAIL;
const cssLevel = options.cssLevel ?? DEFAULT_CSS_LEVEL;

const textVariants = collectTextVariants(element);
const resolvedText = resolveTextByDetail(textVariants, textDetail);

const computedStyle = window.getComputedStyle(element);
const fullCSS = extractFullCSSProperties(computedStyle);
const cssProperties = getElementCSSProperties(computedStyle, cssLevel, fullCSS);

const target: TargetedElement = {
selector: generateSelector(element),
tagName: element.tagName,
id: element.id || undefined,
classes: getElementClasses(element),
innerText: element.innerText || element.textContent || '',
attributes: getElementAttributes(element),
position: getElementPosition(element),
cssProperties: getElementCSSProperties(element),
cssLevel,
cssProperties,
cssComputed: Object.keys(fullCSS).length > 0 ? fullCSS : undefined,
componentInfo: getReactFiberInfo(element),
timestamp: Date.now(),
url: window.location.href,
textDetail,
textVariants,
textContent: textVariants.full,
};

if (resolvedText !== undefined) {
target.innerText = resolvedText;
}

if (!target.textContent && textVariants.visible) {
target.textContent = textVariants.visible;
}

return target;
}

/**
Expand Down
8 changes: 6 additions & 2 deletions packages/chrome-extension/src/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@ export interface TargetedElement {
tagName: string;
id?: string;
classes: string[];
innerText: string;
innerText?: string;
textContent?: string;
textDetail?: 'full' | 'visible' | 'none';
attributes: Record<string, string>;
position: ElementPosition;
cssProperties: CSSProperties;
cssLevel?: 0 | 1 | 2 | 3;
cssProperties?: CSSProperties;
cssComputed?: Record<string, string>;
componentInfo?: ComponentInfo;
timestamp: number;
url: string;
Expand Down
2 changes: 2 additions & 0 deletions packages/server/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@

Server ready for browser extension updates.

- Added dynamic context control (text detail & css levels)

### Patch Changes

- 1c9cef4: Replace jsdom with node-html-parser for better bundling
Expand Down
Loading