-
Notifications
You must be signed in to change notification settings - Fork 42
feat: add dynamic context control for get-pointed-element tool #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
f7637a0
4860ac2
3254237
041a2ba
de8f249
cf3fc32
2e468ab
7cf0678
b5b1d29
cbb4704
4ce493e
ca7a516
4fb84d0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 { | ||
|
@@ -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 | ||
*/ | ||
|
@@ -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 | ||
*/ | ||
|
@@ -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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that confusion with The idea is to completely remove the idea of filtering parameters on the front-end/extension side. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
} | ||
|
||
/** | ||
|
There was a problem hiding this comment.
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?