- refactor state structure to include app, layout, navigation, owner, program, session, and user - add slices for managing program, session, owner, user, layout, navigation, and app states - create context provider for global state with automatic fetching and throttling - implement persistence using IndexedDB with localStorage fallback - add comprehensive README documentation for usage and API
954 lines
25 KiB
JavaScript
Executable File
954 lines
25 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
|
|
/**
|
|
* Oranguru MCP Server
|
|
* Provides documentation and code generation for Oranguru components
|
|
*/
|
|
|
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
import {
|
|
CallToolRequestSchema,
|
|
ListResourcesRequestSchema,
|
|
ListToolsRequestSchema,
|
|
ReadResourceRequestSchema,
|
|
} from '@modelcontextprotocol/sdk/types.js';
|
|
import { readFileSync } from 'fs';
|
|
import { dirname, join } from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = dirname(__filename);
|
|
|
|
// Component documentation data
|
|
const COMPONENTS = {
|
|
Boxer: {
|
|
component: 'Boxer',
|
|
description: 'Advanced combobox/select with virtualization and server-side data support',
|
|
examples: {
|
|
basic: `import { Boxer } from '@warkypublic/oranguru';
|
|
import { useState } from 'react';
|
|
|
|
const sampleData = [
|
|
{ label: 'Apple', value: 'apple' },
|
|
{ label: 'Banana', value: 'banana' },
|
|
{ label: 'Cherry', value: 'cherry' }
|
|
];
|
|
|
|
function MyComponent() {
|
|
const [value, setValue] = useState(null);
|
|
|
|
return (
|
|
<Boxer
|
|
data={sampleData}
|
|
dataSource="local"
|
|
label="Favorite Fruit"
|
|
value={value}
|
|
onChange={setValue}
|
|
searchable
|
|
clearable
|
|
placeholder="Select a fruit"
|
|
/>
|
|
);
|
|
}`,
|
|
multiSelect: `import { Boxer } from '@warkypublic/oranguru';
|
|
import { useState } from 'react';
|
|
|
|
function MultiSelectExample() {
|
|
const [values, setValues] = useState([]);
|
|
|
|
return (
|
|
<Boxer
|
|
data={sampleData}
|
|
dataSource="local"
|
|
label="Favorite Fruits"
|
|
multiSelect
|
|
value={values}
|
|
onChange={setValues}
|
|
searchable
|
|
clearable
|
|
/>
|
|
);
|
|
}`,
|
|
server: `import { Boxer } from '@warkypublic/oranguru';
|
|
|
|
function ServerSideExample() {
|
|
const [value, setValue] = useState(null);
|
|
|
|
const handleAPICall = async ({ page, pageSize, search }) => {
|
|
const response = await fetch(\`/api/items?page=\${page}&size=\${pageSize}&search=\${search}\`);
|
|
const result = await response.json();
|
|
|
|
return {
|
|
data: result.items,
|
|
total: result.total
|
|
};
|
|
};
|
|
|
|
return (
|
|
<Boxer
|
|
dataSource="server"
|
|
label="Server-side Data"
|
|
onAPICall={handleAPICall}
|
|
value={value}
|
|
onChange={setValue}
|
|
pageSize={10}
|
|
searchable
|
|
/>
|
|
);
|
|
}`
|
|
},
|
|
exports: ['Boxer', 'BoxerProvider', 'useBoxerStore'],
|
|
hook: 'useBoxerStore()',
|
|
name: 'Boxer',
|
|
provider: 'BoxerProvider'
|
|
},
|
|
ErrorBoundary: {
|
|
components: ['ReactErrorBoundary', 'ReactBasicErrorBoundary'],
|
|
description: 'React error boundary components for graceful error handling',
|
|
examples: {
|
|
basic: `import { ReactBasicErrorBoundary } from '@warkypublic/oranguru';
|
|
|
|
function App() {
|
|
return (
|
|
<ReactBasicErrorBoundary>
|
|
<MyComponent />
|
|
</ReactBasicErrorBoundary>
|
|
);
|
|
}`,
|
|
full: `import { ReactErrorBoundary } from '@warkypublic/oranguru';
|
|
|
|
function App() {
|
|
const handleReportError = () => {
|
|
console.log('Report error to support');
|
|
};
|
|
|
|
const handleReset = () => {
|
|
console.log('Reset application state');
|
|
window.location.reload();
|
|
};
|
|
|
|
const handleRetry = () => {
|
|
console.log('Retry failed operation');
|
|
};
|
|
|
|
return (
|
|
<ReactErrorBoundary
|
|
namespace="main-app"
|
|
reportAPI="/api/errors/report"
|
|
onReportClick={handleReportError}
|
|
onResetClick={handleReset}
|
|
onRetryClick={handleRetry}
|
|
>
|
|
<MyApp />
|
|
</ReactErrorBoundary>
|
|
);
|
|
}`,
|
|
nested: `import { ReactErrorBoundary } from '@warkypublic/oranguru';
|
|
|
|
// Multiple error boundaries for granular error handling
|
|
function App() {
|
|
return (
|
|
<ReactErrorBoundary namespace="app">
|
|
<Header />
|
|
<ReactErrorBoundary namespace="sidebar">
|
|
<Sidebar />
|
|
</ReactErrorBoundary>
|
|
<ReactErrorBoundary namespace="main-content">
|
|
<MainContent />
|
|
</ReactErrorBoundary>
|
|
</ReactErrorBoundary>
|
|
);
|
|
}`,
|
|
globalConfig: `import { SetErrorBoundaryOptions } from '@warkypublic/oranguru';
|
|
|
|
// Configure error boundary globally
|
|
SetErrorBoundaryOptions({
|
|
disabled: false, // Set to true to pass through errors (dev mode)
|
|
onError: (error, errorInfo) => {
|
|
console.error('Global error handler:', error);
|
|
// Send to analytics service
|
|
analytics.trackError(error, errorInfo);
|
|
}
|
|
});`
|
|
},
|
|
exports: ['ReactErrorBoundary', 'ReactBasicErrorBoundary', 'SetErrorBoundaryOptions', 'GetErrorBoundaryOptions'],
|
|
name: 'ErrorBoundary'
|
|
},
|
|
Former: {
|
|
component: 'Former',
|
|
description: 'Form component with React Hook Form integration and validation',
|
|
examples: {
|
|
basic: `import { Former } from '@warkypublic/oranguru';
|
|
import { useRef } from 'react';
|
|
import { Controller } from 'react-hook-form';
|
|
|
|
function BasicForm() {
|
|
const formRef = useRef(null);
|
|
|
|
return (
|
|
<Former
|
|
ref={formRef}
|
|
primeData={{ name: '', email: '' }}
|
|
onSave={async (data) => {
|
|
console.log('Saving:', data);
|
|
await fetch('/api/save', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data)
|
|
});
|
|
}}
|
|
>
|
|
<Controller
|
|
name="name"
|
|
render={({ field }) => (
|
|
<input {...field} placeholder="Name" />
|
|
)}
|
|
rules={{ required: 'Name is required' }}
|
|
/>
|
|
<Controller
|
|
name="email"
|
|
render={({ field }) => (
|
|
<input {...field} type="email" placeholder="Email" />
|
|
)}
|
|
rules={{ required: 'Email is required' }}
|
|
/>
|
|
<button type="submit">Save</button>
|
|
</Former>
|
|
);
|
|
}`,
|
|
withWrapper: `import { Former, FormerModel } from '@warkypublic/oranguru';
|
|
import { useState } from 'react';
|
|
|
|
function ModalForm() {
|
|
const [opened, setOpened] = useState(false);
|
|
|
|
return (
|
|
<>
|
|
<button onClick={() => setOpened(true)}>Open Form</button>
|
|
|
|
<FormerModel
|
|
former={{ request: 'insert' }}
|
|
opened={opened}
|
|
onClose={() => setOpened(false)}
|
|
>
|
|
<Controller
|
|
name="title"
|
|
render={({ field }) => <input {...field} />}
|
|
/>
|
|
</FormerModel>
|
|
</>
|
|
);
|
|
}`,
|
|
withAPI: `import { Former, FormerRestHeadSpecAPI } from '@warkypublic/oranguru';
|
|
|
|
function APIForm() {
|
|
return (
|
|
<Former
|
|
request="update"
|
|
uniqueKeyField="id"
|
|
primeData={{ id: 123 }}
|
|
onAPICall={FormerRestHeadSpecAPI({
|
|
url: 'https://api.example.com/items',
|
|
authToken: 'your-token'
|
|
})}
|
|
>
|
|
{/* Form fields */}
|
|
</Former>
|
|
);
|
|
}`,
|
|
customLayout: `import { Former } from '@warkypublic/oranguru';
|
|
|
|
function CustomLayoutForm() {
|
|
return (
|
|
<Former
|
|
layout={{
|
|
title: 'Edit User Profile',
|
|
buttonArea: 'bottom',
|
|
buttonAreaGroupProps: { justify: 'space-between' }
|
|
}}
|
|
primeData={{ username: '', bio: '' }}
|
|
>
|
|
{/* Form fields */}
|
|
</Former>
|
|
);
|
|
}`,
|
|
refMethods: `import { Former } from '@warkypublic/oranguru';
|
|
import { useRef } from 'react';
|
|
|
|
function FormWithRef() {
|
|
const formRef = useRef(null);
|
|
|
|
const handleValidate = async () => {
|
|
const isValid = await formRef.current?.validate();
|
|
console.log('Form valid:', isValid);
|
|
};
|
|
|
|
const handleSave = async () => {
|
|
const result = await formRef.current?.save();
|
|
console.log('Save result:', result);
|
|
};
|
|
|
|
const handleReset = () => {
|
|
formRef.current?.reset();
|
|
};
|
|
|
|
return (
|
|
<div>
|
|
<Former ref={formRef} primeData={{}}>
|
|
{/* Form fields */}
|
|
</Former>
|
|
<button onClick={handleValidate}>Validate</button>
|
|
<button onClick={handleSave}>Save</button>
|
|
<button onClick={handleReset}>Reset</button>
|
|
</div>
|
|
);
|
|
}`
|
|
},
|
|
exports: ['Former', 'FormerDialog', 'FormerModel', 'FormerPopover', 'FormerRestHeadSpecAPI'],
|
|
name: 'Former',
|
|
wrappers: ['FormerDialog', 'FormerModel', 'FormerPopover']
|
|
},
|
|
FormerControllers: {
|
|
controls: ['TextInputCtrl', 'PasswordInputCtrl', 'NativeSelectCtrl', 'TextAreaCtrl', 'SwitchCtrl', 'ButtonCtrl', 'IconButtonCtrl'],
|
|
description: 'Pre-built form input controls for use with Former',
|
|
examples: {
|
|
basic: `import { TextInputCtrl, PasswordInputCtrl, NativeSelectCtrl, ButtonCtrl } from '@warkypublic/oranguru';
|
|
|
|
<Former>
|
|
<TextInputCtrl name="username" label="Username" />
|
|
<PasswordInputCtrl name="password" label="Password" />
|
|
<NativeSelectCtrl name="role" data={['Admin', 'User']} />
|
|
<ButtonCtrl type="submit">Save</ButtonCtrl>
|
|
</Former>`
|
|
},
|
|
exports: ['TextInputCtrl', 'PasswordInputCtrl', 'NativeSelectCtrl', 'TextAreaCtrl', 'SwitchCtrl', 'ButtonCtrl', 'IconButtonCtrl'],
|
|
name: 'FormerControllers'
|
|
},
|
|
GlobalStateStore: {
|
|
description: 'Zustand-based global state management with automatic persistence',
|
|
examples: {
|
|
basic: `import { useGlobalStateStore } from '@warkypublic/oranguru';
|
|
|
|
function MyComponent() {
|
|
const state = useGlobalStateStore();
|
|
|
|
return (
|
|
<div>
|
|
<h1>{state.program.name}</h1>
|
|
<p>User: {state.user.username}</p>
|
|
<p>Email: {state.user.email}</p>
|
|
<p>Connected: {state.session.connected ? 'Yes' : 'No'}</p>
|
|
</div>
|
|
);
|
|
}`,
|
|
provider: `import { GlobalStateStoreProvider } from '@warkypublic/oranguru';
|
|
|
|
function App() {
|
|
return (
|
|
<GlobalStateStoreProvider
|
|
apiURL="https://api.example.com"
|
|
fetchOnMount={true}
|
|
throttleMs={5000}
|
|
>
|
|
<MyApp />
|
|
</GlobalStateStoreProvider>
|
|
);
|
|
}`,
|
|
stateUpdates: `import { useGlobalStateStore } from '@warkypublic/oranguru';
|
|
|
|
function StateControls() {
|
|
const state = useGlobalStateStore();
|
|
|
|
const handleUpdateProgram = () => {
|
|
state.setProgram({
|
|
name: 'My App',
|
|
slug: 'my-app',
|
|
description: 'A great application'
|
|
});
|
|
};
|
|
|
|
const handleUpdateUser = () => {
|
|
state.setUser({
|
|
username: 'john_doe',
|
|
email: 'john@example.com',
|
|
fullNames: 'John Doe'
|
|
});
|
|
};
|
|
|
|
const handleToggleTheme = () => {
|
|
state.setUser({
|
|
theme: {
|
|
...state.user.theme,
|
|
darkMode: !state.user.theme?.darkMode
|
|
}
|
|
});
|
|
};
|
|
|
|
return (
|
|
<div>
|
|
<button onClick={handleUpdateProgram}>Update Program</button>
|
|
<button onClick={handleUpdateUser}>Update User</button>
|
|
<button onClick={handleToggleTheme}>Toggle Dark Mode</button>
|
|
</div>
|
|
);
|
|
}`,
|
|
layout: `import { useGlobalStateStore } from '@warkypublic/oranguru';
|
|
|
|
function LayoutControls() {
|
|
const state = useGlobalStateStore();
|
|
|
|
return (
|
|
<div>
|
|
<button onClick={() => state.setLeftBar({ open: !state.layout.leftBar.open })}>
|
|
Toggle Left Bar
|
|
</button>
|
|
<button onClick={() => state.setRightBar({ open: !state.layout.rightBar.open })}>
|
|
Toggle Right Bar
|
|
</button>
|
|
<button onClick={() => state.setLeftBar({ pinned: true, size: 250 })}>
|
|
Pin & Resize Left Bar
|
|
</button>
|
|
</div>
|
|
);
|
|
}`,
|
|
outsideReact: `import { GlobalStateStore } from '@warkypublic/oranguru';
|
|
|
|
// Access state outside React components
|
|
const currentState = GlobalStateStore.getState();
|
|
console.log('API URL:', currentState.session.apiURL);
|
|
|
|
// Update state outside React
|
|
GlobalStateStore.getState().setAuthToken('new-token');
|
|
GlobalStateStore.getState().setApiURL('https://new-api.com');
|
|
|
|
// Subscribe to changes
|
|
const unsubscribe = GlobalStateStore.subscribe(
|
|
(state) => state.session.connected,
|
|
(connected) => console.log('Connected:', connected)
|
|
);`
|
|
},
|
|
exports: ['GlobalStateStore', 'GlobalStateStoreProvider', 'useGlobalStateStore', 'useGlobalStateStoreContext'],
|
|
hook: 'useGlobalStateStore()',
|
|
name: 'GlobalStateStore',
|
|
provider: 'GlobalStateStoreProvider',
|
|
store: 'GlobalStateStore'
|
|
},
|
|
Gridler: {
|
|
adaptors: ['LocalDataAdaptor', 'APIAdaptorForGoLangv2', 'FormAdaptor'],
|
|
component: 'Gridler',
|
|
description: 'Powerful data grid component with sorting, filtering, and pagination',
|
|
examples: {
|
|
basic: `import { Gridler } from '@warkypublic/oranguru';
|
|
|
|
const columns = [
|
|
{ id: 'name', title: 'Name', width: 200 },
|
|
{ id: 'email', title: 'Email', width: 250 },
|
|
{ id: 'role', title: 'Role', width: 150 }
|
|
];
|
|
|
|
const data = [
|
|
{ name: 'John Doe', email: 'john@example.com', role: 'Admin' },
|
|
{ name: 'Jane Smith', email: 'jane@example.com', role: 'User' }
|
|
];
|
|
|
|
function GridExample() {
|
|
return (
|
|
<Gridler columns={columns} uniqueid="my-grid">
|
|
<Gridler.LocalDataAdaptor data={data} />
|
|
</Gridler>
|
|
);
|
|
}`,
|
|
customCell: `// Custom cell rendering with icons
|
|
const columns = [
|
|
{
|
|
id: 'status',
|
|
title: 'Status',
|
|
width: 100,
|
|
Cell: (row) => {
|
|
const icon = row?.active ? '✅' : '❌';
|
|
return {
|
|
data: \`\${icon} \${row?.status}\`,
|
|
displayData: \`\${icon} \${row?.status}\`
|
|
};
|
|
}
|
|
},
|
|
{ id: 'name', title: 'Name', width: 200 }
|
|
];`,
|
|
api: `import { Gridler, GlidlerAPIAdaptorForGoLangv2 } from '@warkypublic/oranguru';
|
|
import { useRef } from 'react';
|
|
|
|
function APIGridExample() {
|
|
const gridRef = useRef(null);
|
|
|
|
const columns = [
|
|
{ id: 'id', title: 'ID', width: 100 },
|
|
{ id: 'name', title: 'Name', width: 200 },
|
|
{ id: 'status', title: 'Status', width: 150 }
|
|
];
|
|
|
|
return (
|
|
<Gridler
|
|
columns={columns}
|
|
uniqueid="api-grid"
|
|
ref={gridRef}
|
|
selectMode="row"
|
|
searchStr={searchTerm}
|
|
onChange={(values) => console.log('Selected:', values)}
|
|
>
|
|
<GlidlerAPIAdaptorForGoLangv2
|
|
url="https://api.example.com/data"
|
|
authtoken="your-api-key"
|
|
/>
|
|
</Gridler>
|
|
);
|
|
}`,
|
|
withForm: `// Gridler with Former integration for inline editing
|
|
import { Gridler, GlidlerAPIAdaptorForGoLangv2 } from '@warkypublic/oranguru';
|
|
import { FormerDialog } from '@warkypublic/oranguru';
|
|
import { TextInputCtrl, NativeSelectCtrl } from '@warkypublic/oranguru';
|
|
import { useState, useRef } from 'react';
|
|
|
|
function EditableGrid() {
|
|
const gridRef = useRef(null);
|
|
const [formProps, setFormProps] = useState({
|
|
opened: false,
|
|
request: null,
|
|
values: null,
|
|
onClose: () => setFormProps(prev => ({
|
|
...prev,
|
|
opened: false,
|
|
request: null,
|
|
values: null
|
|
})),
|
|
onChange: (request, data) => {
|
|
gridRef.current?.refresh({ value: data });
|
|
}
|
|
});
|
|
|
|
const columns = [
|
|
{ id: 'id', title: 'ID', width: 100 },
|
|
{ id: 'name', title: 'Name', width: 200 },
|
|
{ id: 'type', title: 'Type', width: 150 }
|
|
];
|
|
|
|
return (
|
|
<>
|
|
<Gridler
|
|
ref={gridRef}
|
|
columns={columns}
|
|
uniqueid="editable-grid"
|
|
selectMode="row"
|
|
>
|
|
<GlidlerAPIAdaptorForGoLangv2
|
|
url="https://api.example.com/items"
|
|
authtoken="your-token"
|
|
/>
|
|
<Gridler.FormAdaptor
|
|
changeOnActiveClick={true}
|
|
descriptionField="name"
|
|
onRequestForm={(request, data) => {
|
|
setFormProps(prev => ({
|
|
...prev,
|
|
opened: true,
|
|
request: request,
|
|
values: data
|
|
}));
|
|
}}
|
|
/>
|
|
</Gridler>
|
|
|
|
<FormerDialog
|
|
former={{
|
|
request: formProps.request ?? "insert",
|
|
values: formProps.values
|
|
}}
|
|
opened={formProps.opened}
|
|
onClose={formProps.onClose}
|
|
title="Edit Item"
|
|
>
|
|
<TextInputCtrl label="Name" name="name" />
|
|
<TextInputCtrl label="Description" name="description" />
|
|
<NativeSelectCtrl
|
|
label="Type"
|
|
name="type"
|
|
data={["Type A", "Type B", "Type C"]}
|
|
/>
|
|
</FormerDialog>
|
|
</>
|
|
);
|
|
}`,
|
|
refMethods: `// Using Gridler ref methods for programmatic control
|
|
import { Gridler } from '@warkypublic/oranguru';
|
|
import { useRef } from 'react';
|
|
|
|
function GridWithControls() {
|
|
const gridRef = useRef(null);
|
|
|
|
return (
|
|
<>
|
|
<Gridler ref={gridRef} columns={columns} uniqueid="controlled-grid">
|
|
<Gridler.LocalDataAdaptor data={data} />
|
|
</Gridler>
|
|
|
|
<div>
|
|
<button onClick={() => gridRef.current?.refresh()}>
|
|
Refresh Grid
|
|
</button>
|
|
<button onClick={() => gridRef.current?.selectRow(123)}>
|
|
Select Row 123
|
|
</button>
|
|
<button onClick={() => gridRef.current?.scrollToRow(456)}>
|
|
Scroll to Row 456
|
|
</button>
|
|
<button onClick={() => gridRef.current?.reloadRow(789)}>
|
|
Reload Row 789
|
|
</button>
|
|
</div>
|
|
</>
|
|
);
|
|
}`,
|
|
sections: `// Gridler with custom side sections
|
|
import { Gridler } from '@warkypublic/oranguru';
|
|
import { useState } from 'react';
|
|
|
|
function GridWithSections() {
|
|
const [sections, setSections] = useState({
|
|
top: <div style={{ backgroundColor: 'purple', height: '20px' }}>Top Bar</div>,
|
|
bottom: <div style={{ backgroundColor: 'teal', height: '25px' }}>Bottom Bar</div>,
|
|
left: <div style={{ backgroundColor: 'orange', width: '20px' }}>L</div>,
|
|
right: <div style={{ backgroundColor: 'green', width: '20px' }}>R</div>
|
|
});
|
|
|
|
return (
|
|
<Gridler
|
|
columns={columns}
|
|
uniqueid="sections-grid"
|
|
sections={{ ...sections, rightElementDisabled: false }}
|
|
>
|
|
<Gridler.LocalDataAdaptor data={data} />
|
|
</Gridler>
|
|
);
|
|
}`
|
|
},
|
|
exports: ['Gridler', 'GlidlerLocalDataAdaptor', 'GlidlerAPIAdaptorForGoLangv2', 'GlidlerFormAdaptor'],
|
|
hook: 'useGridlerStore()',
|
|
name: 'Gridler'
|
|
},
|
|
MantineBetterMenu: {
|
|
description: 'Enhanced context menus with better positioning and visibility control',
|
|
examples: {
|
|
provider: `import { MantineBetterMenusProvider } from '@warkypublic/oranguru';
|
|
import { MantineProvider } from '@mantine/core';
|
|
|
|
function App() {
|
|
return (
|
|
<MantineProvider>
|
|
<MantineBetterMenusProvider providerID="main">
|
|
<YourApp />
|
|
</MantineBetterMenusProvider>
|
|
</MantineProvider>
|
|
);
|
|
}`,
|
|
contextMenu: `import { useMantineBetterMenus } from '@warkypublic/oranguru';
|
|
|
|
function MyComponent() {
|
|
const { show, hide } = useMantineBetterMenus();
|
|
|
|
const handleContextMenu = (e) => {
|
|
e.preventDefault();
|
|
|
|
show('context-menu', {
|
|
x: e.clientX,
|
|
y: e.clientY,
|
|
items: [
|
|
{
|
|
label: 'Edit',
|
|
onClick: () => console.log('Edit clicked')
|
|
},
|
|
{
|
|
label: 'Copy',
|
|
onClick: () => console.log('Copy clicked')
|
|
},
|
|
{
|
|
isDivider: true
|
|
},
|
|
{
|
|
label: 'Delete',
|
|
onClick: () => console.log('Delete clicked'),
|
|
color: 'red'
|
|
}
|
|
]
|
|
});
|
|
};
|
|
|
|
return (
|
|
<div onContextMenu={handleContextMenu}>
|
|
Right-click me for a context menu
|
|
</div>
|
|
);
|
|
}`,
|
|
asyncActions: `import { useMantineBetterMenus } from '@warkypublic/oranguru';
|
|
|
|
function AsyncMenuExample() {
|
|
const { show } = useMantineBetterMenus();
|
|
|
|
const handleClick = (e) => {
|
|
show('async-menu', {
|
|
x: e.clientX,
|
|
y: e.clientY,
|
|
items: [
|
|
{
|
|
label: 'Save',
|
|
onClickAsync: async () => {
|
|
await fetch('/api/save', { method: 'POST' });
|
|
console.log('Saved successfully');
|
|
}
|
|
},
|
|
{
|
|
label: 'Load Data',
|
|
onClickAsync: async () => {
|
|
const data = await fetch('/api/data').then(r => r.json());
|
|
console.log('Data loaded:', data);
|
|
}
|
|
}
|
|
]
|
|
});
|
|
};
|
|
|
|
return <button onClick={handleClick}>Show Menu</button>;
|
|
}`,
|
|
customRenderer: `import { useMantineBetterMenus } from '@warkypublic/oranguru';
|
|
|
|
function CustomMenuExample() {
|
|
const { show } = useMantineBetterMenus();
|
|
|
|
const handleClick = (e) => {
|
|
show('custom-menu', {
|
|
x: e.clientX,
|
|
y: e.clientY,
|
|
items: [
|
|
{
|
|
renderer: ({ loading }) => (
|
|
<div style={{ padding: '8px 12px', color: 'blue' }}>
|
|
{loading ? 'Processing...' : 'Custom Item'}
|
|
</div>
|
|
)
|
|
}
|
|
]
|
|
});
|
|
};
|
|
|
|
return <button onClick={handleClick}>Custom Menu</button>;
|
|
}`
|
|
},
|
|
exports: ['MantineBetterMenusProvider', 'useMantineBetterMenus'],
|
|
hook: 'useMantineBetterMenus()',
|
|
name: 'MantineBetterMenu',
|
|
provider: 'MantineBetterMenusProvider'
|
|
}
|
|
};
|
|
|
|
class OranguruMCPServer {
|
|
constructor() {
|
|
this.server = new Server(
|
|
{
|
|
name: 'oranguru-docs',
|
|
version: '0.0.31',
|
|
},
|
|
{
|
|
capabilities: {
|
|
resources: {},
|
|
tools: {},
|
|
},
|
|
}
|
|
);
|
|
|
|
this.setupHandlers();
|
|
this.server.onerror = (error) => console.error('[MCP Error]', error);
|
|
process.on('SIGINT', async () => {
|
|
await this.server.close();
|
|
process.exit(0);
|
|
});
|
|
}
|
|
|
|
async run() {
|
|
const transport = new StdioServerTransport();
|
|
await this.server.connect(transport);
|
|
console.error('Oranguru MCP server running on stdio');
|
|
}
|
|
|
|
setupHandlers() {
|
|
// List available tools
|
|
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
tools: [
|
|
{
|
|
description: 'Get documentation for a specific Oranguru component',
|
|
inputSchema: {
|
|
properties: {
|
|
component: {
|
|
description: 'The component name',
|
|
enum: Object.keys(COMPONENTS),
|
|
type: 'string',
|
|
},
|
|
},
|
|
required: ['component'],
|
|
type: 'object',
|
|
},
|
|
name: 'get_component_docs',
|
|
},
|
|
{
|
|
description: 'Generate code example for a specific Oranguru component',
|
|
inputSchema: {
|
|
properties: {
|
|
component: {
|
|
description: 'The component name',
|
|
enum: Object.keys(COMPONENTS),
|
|
type: 'string',
|
|
},
|
|
variant: {
|
|
description: "Example variant (e.g., 'basic', 'local', 'server')",
|
|
type: 'string',
|
|
},
|
|
},
|
|
required: ['component'],
|
|
type: 'object',
|
|
},
|
|
name: 'get_component_example',
|
|
},
|
|
{
|
|
description: 'List all available Oranguru components',
|
|
inputSchema: {
|
|
properties: {},
|
|
type: 'object',
|
|
},
|
|
name: 'list_components',
|
|
},
|
|
],
|
|
}));
|
|
|
|
// Handle tool calls
|
|
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
const { arguments: args, name } = request.params;
|
|
|
|
switch (name) {
|
|
case 'get_component_docs':
|
|
if (!args.component || !COMPONENTS[args.component]) {
|
|
throw new Error(`Component ${args.component} not found`);
|
|
}
|
|
return {
|
|
content: [
|
|
{
|
|
text: JSON.stringify(COMPONENTS[args.component], null, 2),
|
|
type: 'text',
|
|
},
|
|
],
|
|
};
|
|
|
|
case 'get_component_example':
|
|
if (!args.component || !COMPONENTS[args.component]) {
|
|
throw new Error(`Component ${args.component} not found`);
|
|
}
|
|
const component = COMPONENTS[args.component];
|
|
const variant = args.variant || Object.keys(component.examples)[0];
|
|
const example = component.examples[variant];
|
|
|
|
if (!example) {
|
|
throw new Error(`Variant ${variant} not found for ${args.component}`);
|
|
}
|
|
|
|
return {
|
|
content: [
|
|
{
|
|
text: example,
|
|
type: 'text',
|
|
},
|
|
],
|
|
};
|
|
|
|
case 'list_components':
|
|
return {
|
|
content: [
|
|
{
|
|
text: JSON.stringify(
|
|
Object.entries(COMPONENTS).map(([key, comp]) => ({
|
|
description: comp.description,
|
|
exports: comp.exports,
|
|
name: key,
|
|
})),
|
|
null,
|
|
2
|
|
),
|
|
type: 'text',
|
|
},
|
|
],
|
|
};
|
|
|
|
default:
|
|
throw new Error(`Unknown tool: ${name}`);
|
|
}
|
|
});
|
|
|
|
// List resources
|
|
this.server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
resources: [
|
|
{
|
|
description: 'Main documentation for the Oranguru library',
|
|
mimeType: 'text/markdown',
|
|
name: 'Oranguru Documentation',
|
|
uri: 'oranguru://docs/readme',
|
|
},
|
|
{
|
|
description: 'List of all available components',
|
|
mimeType: 'application/json',
|
|
name: 'Component List',
|
|
uri: 'oranguru://docs/components',
|
|
},
|
|
],
|
|
}));
|
|
|
|
// Read resources
|
|
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
const { uri } = request.params;
|
|
|
|
if (uri === 'oranguru://docs/readme') {
|
|
const readmePath = join(__dirname, '..', 'README.md');
|
|
const readme = readFileSync(readmePath, 'utf-8');
|
|
return {
|
|
contents: [
|
|
{
|
|
mimeType: 'text/markdown',
|
|
text: readme,
|
|
uri,
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
if (uri === 'oranguru://docs/components') {
|
|
return {
|
|
contents: [
|
|
{
|
|
mimeType: 'application/json',
|
|
text: JSON.stringify(
|
|
Object.entries(COMPONENTS).map(([key, comp]) => ({
|
|
description: comp.description,
|
|
exports: comp.exports,
|
|
name: key,
|
|
})),
|
|
null,
|
|
2
|
|
),
|
|
uri,
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
throw new Error(`Resource not found: ${uri}`);
|
|
});
|
|
}
|
|
}
|
|
|
|
const server = new OranguruMCPServer();
|
|
server.run().catch(console.error);
|