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
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(
Copy link
Collaborator

Choose a reason for hiding this comment

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

Hey @ashtonchew,

I see that adaptTargetToElement still exists, my mistake. adaptTargetToElement is no longer used, and should be removed.
Would you like to remove it in your PR or do you want me to clean-up in a separate commit so you can rebase?

element: HTMLElement,
options: ElementSerializationOptions = {},
): TargetedElement {
const textDetail = options.textDetail ?? DEFAULT_TEXT_DETAIL;
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think that confusion with adaptTargetToElement still existing led to maintain the notion of detail parameter on chrome-extension side.

The idea is to completely remove the idea of filtering parameters on the front-end/extension side.

Copy link
Collaborator

Choose a reason for hiding this comment

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

(this will probably lead to a 0 added line to chrome-extension in this PR)

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