To create a component that works with the affordance system, you need JavaScript/TypeScript code that exports a class implementing the AffordanceComponent
interface. This code can come from:
- MCP file sources (recommended) - Use MCP tools to create/manage component files
- Local project files - Files in the
/frontend/components/affordances/
directory - Any accessible source - The system will try MCP first, then fall back to local files
interface AffordanceMethodSchema {
description: string;
inputSchema: {
type: "object";
properties: Record<string, any>;
required?: string[];
};
}
interface AffordanceComponent {
mount(container: HTMLElement, config: AffordanceConfig, mcpClientPool?: any): Promise<void>;
unmount(): Promise<void>;
getPublicMethods(): Record<string, AffordanceMethodSchema>;
[methodName: string]: any; // Methods can be called directly on the component
}
Key Points:
getPublicMethods()
returns method schemas (not function references)- Methods are called directly on the component instance
- The schemas help the assistant understand what methods are available and how to call them
-
Schema Definition:
getPublicMethods()
returns an object describing available methods:getPublicMethods(): Record<string, AffordanceMethodSchema> { return { methodName: { description: "What this method does", inputSchema: { type: "object", properties: { param1: { type: "string", description: "First parameter" }, param2: { type: "number", description: "Second parameter" } }, required: ["param1"] } } }; } -
Method Implementation: Implement the actual methods on your class:
methodName(param1: string, param2?: number): any { // Your method implementation return result; } -
Method Calling: The affordance manager calls methods directly on your component:
// Assistant calls this: await call_affordance_method(domId, 'methodName', ['value1', 42]); // Which translates to: component.methodName('value1', 42);
/** @jsxImportSource https://esm.sh/react@18.2.0 */
import React from "https://esm.sh/react@18.2.0";
import { createRoot } from "https://esm.sh/react-dom@18.2.0/client";
import { AffordanceComponent, AffordanceConfig, AffordanceMethodSchema } from "../../../shared/affordance-types.ts";
// Your React component
const MyComponent: React.FC<{message: string}> = ({ message }) => {
return <div style={{padding: '1rem'}}>{message}</div>;
};
// Affordance wrapper class (REQUIRED)
export default class MyAffordance implements AffordanceComponent {
private root: any = null;
private container: HTMLElement | null = null;
private message: string = "Hello!";
async mount(container: HTMLElement, config: AffordanceConfig, mcpClientPool?: any): Promise<void> {
this.container = container;
this.message = config.message || "Hello!";
this.root = createRoot(container);
this.root.render(<MyComponent message={this.message} />);
}
async unmount(): Promise<void> {
if (this.root) {
this.root.unmount();
this.root = null;
}
this.container = null;
}
getPublicMethods(): Record<string, AffordanceMethodSchema> {
return {
setMessage: {
description: "Set a new message to display",
inputSchema: {
type: "object",
properties: {
newMessage: {
type: "string",
description: "The new message to display"
}
},
required: ["newMessage"]
}
},
getMessage: {
description: "Get the current message",
inputSchema: {
type: "object",
properties: {},
required: []
}
}
};
}
// These methods will be called directly by the affordance manager
setMessage(newMessage: string): void {
this.message = newMessage;
if (this.root) {
this.root.render(<MyComponent message={this.message} />);
}
}
getMessage(): string {
return this.message;
}
}
- Content: Must contain JavaScript/TypeScript code that exports a class
- Export: Must have a default export of the affordance class
- Dependencies: Use ESM imports from
https://esm.sh/
for compatibility - React: Pin React version to 18.2.0 for consistency
- Source: Can be from MCP files, local files, or any accessible source
The system tries to load components in this order:
- MCP source - Uses
files-get
tool to fetch from MCP server - Local file - Falls back to local project files
- Error - If neither source works, provides detailed error message
No example components are currently included in the project. You can create your own affordance components by following the interface described above. The components should be saved as MCP files or created in the /frontend/components/affordances/
directory.
- Best for: Modals, dialogs, forms, image viewers
- Config:
modal: true/false
,backdrop: true/false
,position: 'center'|'top'|'bottom'
- Size: Use
maxWidth
,maxHeight
for responsive design
- Best for: Tool palettes, navigation, dashboards, file browsers
- Config:
position: 'left'|'right'
,width: '300px'
,collapsible: true
- Size: Fixed width recommended (250px-400px)
- Best for: Status indicators, breadcrumbs, quick actions
- Config:
position: 'left'|'right'
,priority: number
- Size: Keep compact, height auto-adjusts
- Best for: Status bars, progress indicators, quick stats
- Config:
position: 'left'|'right'
,priority: number
- Size: Keep compact, integrates with existing footer
- Best for: Widgets, charts, interactive content in chat
- Config:
width: '100%'
,height: '300px'
- Size: Responsive width, fixed height often works best
// 1. Create component using MCP tools (files-create, etc.)
// 2. Register the component by file key
const id = await attach_affordance('sidebar',
'components/my-dashboard.js', // MCP file key
{ title: 'My Dashboard', position: 'right' }
);
// 1. Create component file in /frontend/components/affordances/
// 2. Register the component by local path
const id = await attach_affordance('overlay',
'/frontend/components/affordances/MyComponent.tsx',
{ title: 'My Component', modal: true }
);
// Register a local component file const id = await register_affordance('overlay', '/frontend/components/affordances/TestAffordance.tsx', { title: 'Test Widget' } );
### Check Methods and Interact
```javascript
// Check available methods
const methods = await call_affordance_method(id, 'getPublicMethods', []);
// Call component methods
await call_affordance_method(id, 'setMessage', ['Hello World!']);
class MyAffordance implements AffordanceComponent {
private data: any[] = [];
addData(item: any): void {
this.data.push(item);
this.render(); // Re-render with new data
}
private render(): void {
if (this.root) {
this.root.render(<MyComponent data={this.data} />);
}
}
}
async mount(container: HTMLElement, config: AffordanceConfig): Promise<void> {
// Extract config with defaults
const title = config.title || "Default Title";
const theme = config.theme || "light";
const autoRefresh = config.autoRefresh || false;
// Use config in component
this.root.render(<MyComponent title={title} theme={theme} />);
}
async unmount(): Promise<void> {
// Clear timers
if (this.timer) clearInterval(this.timer);
// Remove event listeners
if (this.eventHandler) {
document.removeEventListener('click', this.eventHandler);
}
// Unmount React
if (this.root) {
this.root.unmount();
this.root = null;
}
}
The affordance system will catch and report:
- Module loading errors (wrong file type, syntax errors)
- Missing interface methods
- Runtime errors in mount/unmount
- Method call errors
Always test your components with:
// Register component
const id = await register_affordance('overlay', '/path/to/component.tsx', {title: 'Test'});
// Test methods
await call_affordance_method(id, 'getPublicMethods', []);
- Check Console: All affordance operations are logged with
[AffordanceManager]
prefix - Use getPublicMethods: Always call this first to see available methods
- Test Incrementally: Start with simple components, add complexity gradually
- Validate Config: Check that your config properties are being used correctly
- React DevTools: Use browser dev tools to inspect React component tree