@alaarab/ogrid-mcp 2.15.1 → 2.15.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @alaarab/ogrid-mcp
2
2
 
3
- MCP server that lets AI editors search and retrieve OGrid documentation across all supported frameworks.
3
+ MCP server that lets AI editors (Claude Code, Cursor, VS Code Copilot, etc.) search and retrieve OGrid documentation and, optionally, inspect and drive live OGrid instances in your running app through a local testing bridge.
4
4
 
5
5
  ## Install
6
6
 
@@ -17,7 +17,64 @@ Add to your editor's MCP configuration:
17
17
  Or run directly:
18
18
 
19
19
  ```bash
20
- npx @alaarab/ogrid-mcp
20
+ npx @alaarab/ogrid-mcp # docs server (stdio)
21
+ npx @alaarab/ogrid-mcp --bridge # docs server + live testing bridge on port 7890
22
+ npx @alaarab/ogrid-mcp --version # print version
21
23
  ```
22
24
 
23
- See the [OGrid docs](https://alaarab.github.io/ogrid/) for full documentation.
25
+ ## Tools
26
+
27
+ Documentation tools (always available):
28
+
29
+ | Tool | Description |
30
+ | --- | --- |
31
+ | `search_docs` | Search OGrid documentation by keyword. Returns matching docs with title, description, and content excerpt. |
32
+ | `list_docs` | List available documentation pages, optionally filtered by category (`features`, `getting-started`, `guides`, `api`). |
33
+ | `get_docs` | Get the full content of a documentation page by path (e.g. `features/sorting`, `api/column-def`). |
34
+ | `get_code_example` | Find code examples from the docs matching a query, optionally filtered by framework. |
35
+ | `detect_version` | Detect which OGrid version and framework a project uses by reading its `package.json`. |
36
+
37
+ Live-bridge tools (available when the bridge is enabled):
38
+
39
+ | Tool | Description |
40
+ | --- | --- |
41
+ | `list_grids` | List OGrid instances currently connected to the bridge: grid IDs, row counts, page info, last-seen timestamps. |
42
+ | `get_grid_state` | Get a connected grid's current state: displayed rows, columns, sort, filters, pagination, and selection. |
43
+ | `send_grid_command` | Send a command to a connected grid (sort, filter, paginate, edit a cell, …) and wait for the result. |
44
+
45
+ ## Resources
46
+
47
+ | URI | Description |
48
+ | --- | --- |
49
+ | `ogrid://quick-reference` | Key props, install commands, and common patterns. |
50
+ | `ogrid://migration-guide` | Full migration guide from AG Grid with side-by-side API mapping. |
51
+ | `ogrid://docs/{path}` | Any documentation page by path. |
52
+
53
+ ## Live testing bridge
54
+
55
+ The bridge lets an AI assistant observe and drive real OGrid instances in your running app — useful for agentic testing and debugging.
56
+
57
+ 1. Start the server with the bridge enabled: `npx @alaarab/ogrid-mcp --bridge` (or set `OGRID_BRIDGE_PORT` to pick a port; `--bridge` defaults to `7890`).
58
+ 2. In your app, connect a grid to the bridge:
59
+
60
+ ```tsx
61
+ import { connectGridToBridge } from '@alaarab/ogrid-mcp/bridge-client';
62
+
63
+ const connection = connectGridToBridge({
64
+ gridId: 'employees',
65
+ getData: () => rows,
66
+ getColumns: () => columns,
67
+ api: gridApiRef.current, // optional: enables send_grid_command actions
68
+ // bridgeUrl: 'http://localhost:7890' (default), pollIntervalMs: 500 (default)
69
+ });
70
+
71
+ // later: connection.disconnect();
72
+ ```
73
+
74
+ 3. The assistant can now call `list_grids`, `get_grid_state`, and `send_grid_command` against your live app.
75
+
76
+ The bridge client polls the local bridge over HTTP and never talks to anything but `bridgeUrl`.
77
+
78
+ ## Documentation
79
+
80
+ See the [OGrid docs](https://alaarab.github.io/ogrid/) — in particular the [MCP guide](https://alaarab.github.io/ogrid/docs/guides/mcp) and [MCP live testing guide](https://alaarab.github.io/ogrid/docs/guides/mcp-live-testing).
@@ -6,11 +6,10 @@ description: API reference for useHeadlessGrid + the spreadsheet hook set (useIn
6
6
 
7
7
  # Headless hooks
8
8
 
9
- OGrid 2.9 ships seven framework-native hooks that expose grid logic without
10
- imposing any chrome. Pair `useHeadlessGrid` with the spreadsheet hooks to
11
- add inline edit, range selection, fill handle, clipboard, undo/redo, and
12
- keyboard navigation to your own table markup (shadcn `<Table>`, plain HTML,
13
- Fluent `<DataGrid>`, anything).
9
+ OGrid exposes hooks that provide the grid logic without imposing any UI.
10
+ Pair `useHeadlessGrid` with the spreadsheet hooks to add inline edit, range
11
+ selection, fill handle, clipboard, undo/redo, and keyboard navigation to your
12
+ own table markup (shadcn `<Table>`, plain HTML, Fluent `<DataGrid>`, anything).
14
13
 
15
14
  All seven hooks ship as React hooks:
16
15
 
@@ -33,6 +33,7 @@ Complete reference for `IOGridProps<T>`, the props accepted by the `OGrid` compo
33
33
  | `onSortChange` | `(sort: { field: string; direction: 'asc' \| 'desc' }) => void` | `undefined` | Callback fired when the user changes the sort. Required for controlled sort mode. |
34
34
  | `defaultSortBy` | `string` | `undefined` | Column ID to sort by initially in uncontrolled mode. |
35
35
  | `defaultSortDirection` | `'asc' \| 'desc'` | `undefined` | Initial sort direction in uncontrolled mode. Used together with `defaultSortBy`. |
36
+ | `workerSort` | `boolean \| 'auto'` | `false` | Offload sorting to a Web Worker to keep the main thread responsive. `true` always uses the worker; `'auto'` uses it past ~5000 rows. Columns with a custom `compare` fall back to synchronous sort. |
36
37
 
37
38
  ## Filtering
38
39
 
@@ -61,6 +62,7 @@ Complete reference for `IOGridProps<T>`, the props accepted by the `OGrid` compo
61
62
  | `columnOrder` | `string[]` | `undefined` | Controlled column order as an array of column IDs. |
62
63
  | `onColumnOrderChange` | `(order: string[]) => void` | `undefined` | Callback fired when the user reorders columns. |
63
64
  | `onColumnResized` | `(columnId: string, width: number) => void` | `undefined` | Callback fired when the user finishes resizing a column. |
65
+ | `responsiveColumns` | `boolean \| IResponsiveColumnsConfig` | `false` | Auto-hide columns by their `responsivePriority` as the container narrows. `true` uses default breakpoints (576/768/992/1200px); pass a config object for custom thresholds. |
64
66
 
65
67
  ## Editing
66
68
 
@@ -128,12 +130,39 @@ Complete reference for `IOGridProps<T>`, the props accepted by the `OGrid` compo
128
130
  | `rowHeight` | `number` | `36` | Fixed row height in pixels. Affects layout and virtual scrolling row height calculation. |
129
131
  | `density` | `'compact' \| 'normal' \| 'comfortable'` | `'normal'` | Cell spacing preset. Controls cell padding throughout the grid. |
130
132
  | `showRowNumbers` | `boolean` | `false` | Show Excel-style row number column (1, 2, 3...) at the start of the grid. |
133
+ | `cellReferences` | `boolean` | `false` | Excel-style cell references: column-letter headers (A, B, C…), a row-number gutter, and a name box showing the active cell (e.g. "A1"). Implies `showRowNumbers`. |
131
134
  | `className` | `string` | `undefined` | Additional CSS class name applied to the root grid element. |
132
135
  | `toolbar` | `ReactNode` | `undefined` | Custom toolbar content rendered in the left side of the primary toolbar strip. |
133
136
  | `toolbarBelow` | `ReactNode` | `undefined` | Secondary toolbar row rendered below the primary toolbar. Use for active filter chips, breadcrumbs, or other contextual controls. |
134
137
  | `entityLabelPlural` | `string` | `undefined` | Plural label for the entity type (e.g., `"projects"`, `"users"`). Used in status bar text and empty state messages. |
135
138
  | `emptyState` | `{ message?: ReactNode; render?: () => ReactNode }` | `undefined` | Custom empty state configuration. Provide `message` for a simple text override, or `render` for a fully custom empty state component. |
136
139
 
140
+ ## Formulas
141
+
142
+ | Name | Type | Default | Description |
143
+ |------|------|---------|-------------|
144
+ | `formulas` | `boolean` | `false` | Enable Excel-like formulas. When on, cells starting with `=` are evaluated by the built-in formula engine. |
145
+ | `initialFormulas` | `Array<{ col: number; row: number; formula: string }>` | `undefined` | Formulas to load when the engine initializes. |
146
+ | `onFormulaRecalc` | `(result: IRecalcResult) => void` | `undefined` | Called when a recalculation updates cell values (e.g. a cascade from an edited cell). |
147
+ | `formulaFunctions` | `Record<string, IFormulaFunction>` | `undefined` | Custom functions to register with the formula engine. |
148
+ | `namedRanges` | `Record<string, string>` | `undefined` | Named ranges for formulas, mapping a name to a cell/range ref (e.g. `{ Revenue: 'A1:A10' }`). |
149
+ | `sheets` | `Record<string, IGridDataAccessor>` | `undefined` | Accessors for cross-sheet formula references (e.g. `{ Sheet2: accessor }`). |
150
+
151
+ See [Formulas](../features/formulas) for the full guide.
152
+
153
+ ## Multi-Sheet
154
+
155
+ Excel-style sheet tabs along the bottom of the grid.
156
+
157
+ | Name | Type | Default | Description |
158
+ |------|------|---------|-------------|
159
+ | `sheetDefs` | `ISheetDef[]` | `undefined` | Sheet definitions. When set, renders a bottom tab bar (the `SheetTabs` component). |
160
+ | `activeSheet` | `string` | `undefined` | The currently active sheet id. |
161
+ | `onSheetChange` | `(sheetId: string) => void` | `undefined` | Called when the user switches sheets. |
162
+ | `onSheetAdd` | `() => void` | `undefined` | Called when the user clicks the add-sheet button. |
163
+
164
+ See [Multi-Sheet](../features/multi-sheet).
165
+
137
166
  ## Callbacks
138
167
 
139
168
  | Name | Type | Default | Description |
@@ -5,10 +5,13 @@ description: Built-in column types for common data patterns - text, numeric, d
5
5
  ---
6
6
 
7
7
 
8
+
8
9
  # Column Types
9
10
 
10
11
  OGrid provides built-in column types that automatically configure alignment, display formatting, cell editors, and filter types. Set `type` on a column definition to opt in.
11
12
 
13
+ <ColumnTypesDemo />
14
+
12
15
  ## Available Types
13
16
 
14
17
  | Type | Alignment | Default Editor | Default Filter | Display Format |
@@ -5,10 +5,15 @@ description: Touch-friendly interactions using the Pointer Events API for cell s
5
5
  ---
6
6
 
7
7
 
8
+
8
9
  # Mobile Touch Support
9
10
 
10
11
  OGrid uses the [Pointer Events API](https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events) for all drag interactions, providing unified input handling across mouse, touch, and pen devices. All interactions - cell drag-selection, fill handle drag-to-fill, column resize, and column reorder - work on touch devices out of the box with no additional configuration.
11
12
 
13
+ <MobileTouchDemo />
14
+
15
+ The demo above uses the exact same pointer gestures on every device — try it with a mouse here, or open this page on a phone or tablet: the fill and resize handles automatically enlarge under `@media (pointer: coarse)` for finger-sized targets.
16
+
12
17
  ## How It Works
13
18
 
14
19
  ### Pointer Events API
@@ -0,0 +1,91 @@
1
+ ---
2
+ sidebar_position: 6
3
+ title: Multi-Sheet
4
+ description: Excel-style sheet tabs — render a bottom tab bar and switch between multiple grids.
5
+ ---
6
+
7
+
8
+ # Multi-Sheet
9
+
10
+ OGrid can render an Excel-style **sheet tab bar** along the bottom of the grid so users can switch between multiple sheets. Provide `sheetDefs`, track the `activeSheet`, and swap the grid's `data` (and `columns`, if they differ) when the active sheet changes.
11
+
12
+ <MultiSheetDemo />
13
+
14
+ ## Props
15
+
16
+ | Name | Type | Description |
17
+ |------|------|-------------|
18
+ | `sheetDefs` | `ISheetDef[]` | Sheet definitions. When set, the grid renders the bottom tab bar. |
19
+ | `activeSheet` | `string` | The id of the currently active sheet. |
20
+ | `onSheetChange` | `(sheetId: string) => void` | Called when the user clicks a different tab. |
21
+ | `onSheetAdd` | `() => void` | Called when the user clicks the **+** button. Omit to hide it. |
22
+
23
+ `ISheetDef` is a small shape from `@alaarab/ogrid-core`:
24
+
25
+ ```ts
26
+ interface ISheetDef {
27
+ id: string;
28
+ name: string;
29
+ color?: string; // optional tab color (any CSS color)
30
+ }
31
+ ```
32
+
33
+ ## Example
34
+
35
+ The grid is controlled: you own `activeSheet` and pick which data to show.
36
+
37
+ ```tsx
38
+
39
+ const sheets = [
40
+ { id: "q1", name: "Q1" },
41
+ { id: "q2", name: "Q2" },
42
+ { id: "fy", name: "Full Year", color: "#217346" },
43
+ ];
44
+
45
+ const dataBySheet = {
46
+ q1: q1Rows,
47
+ q2: q2Rows,
48
+ fy: fyRows,
49
+ };
50
+
51
+ function Workbook() {
52
+ const [activeSheet, setActiveSheet] = useState("q1");
53
+ return (
54
+ <OGrid
55
+ columns={columns}
56
+ data={dataBySheet[activeSheet]}
57
+ getRowId={(r) => r.id}
58
+ sheetDefs={sheets}
59
+ activeSheet={activeSheet}
60
+ onSheetChange={setActiveSheet}
61
+ onSheetAdd={() => {/* add a sheet to your own state */}}
62
+ />
63
+ );
64
+ }
65
+ ```
66
+
67
+ ## Cross-sheet formulas
68
+
69
+ When [formulas](./formulas) are enabled, the `sheets` prop registers other sheets' data as accessors so a formula can reference another sheet (e.g. `=Q2!A1`):
70
+
71
+ ```tsx
72
+ <OGrid
73
+ /* ...sheet props as above... */
74
+ formulas
75
+ sheets={{ Q2: q2Accessor, FY: fyAccessor }}
76
+ />
77
+ ```
78
+
79
+ ## Standalone tab bar
80
+
81
+ The tab bar is also exported on its own as `SheetTabs` from `@alaarab/ogrid-react`, if you want to render it outside the grid:
82
+
83
+ ```tsx
84
+
85
+ <SheetTabs
86
+ sheets={sheets}
87
+ activeSheet={activeSheet}
88
+ onSheetChange={setActiveSheet}
89
+ onSheetAdd={addSheet}
90
+ />;
91
+ ```
@@ -5,10 +5,13 @@ description: Automatic CSS containment, opt-in Web Worker sort/filter, and colum
5
5
  ---
6
6
 
7
7
 
8
+
8
9
  # Performance
9
10
 
10
11
  A grid that handles 100 rows fine but chokes at 50,000 isn't production-ready. OGrid ships with three complementary features to handle large datasets without framework-level heroics:
11
12
 
13
+ <PerformanceDemo />
14
+
12
15
  | Feature | Opt-in? | What it does |
13
16
  |---------|---------|---------|
14
17
  | **CSS Containment** | No (automatic) | Browser skips layout and paint for off-screen cells |
@@ -5,10 +5,13 @@ description: Optional premium cell editors with zero bundle impact when not inst
5
5
  ---
6
6
 
7
7
 
8
+
8
9
  # Premium Inputs
9
10
 
10
11
  OGrid's premium inputs are optional cell editor components distributed in a separate package. They are fully tree-shakeable and have **zero bundle impact** when not installed -- your grid stays lightweight until you explicitly opt in.
11
12
 
13
+ <PremiumInputsDemo />
14
+
12
15
  ## Installation
13
16
 
14
17
  <Tabs groupId="framework">
@@ -5,10 +5,13 @@ description: Automatically hide columns based on container width using responsiv
5
5
  ---
6
6
 
7
7
 
8
+
8
9
  # Responsive Columns
9
10
 
10
11
  OGrid can automatically hide lower-priority columns when the grid container becomes narrow, keeping the most important data visible on smaller screens. Columns are progressively hidden based on `responsivePriority` values and configurable width breakpoints.
11
12
 
13
+ <ResponsiveColumnsDemo />
14
+
12
15
  ## How It Works
13
16
 
14
17
  1. Assign `responsivePriority` to columns (0 = highest priority, always visible).
@@ -1,13 +1,16 @@
1
1
  ---
2
2
  sidebar_position: 16
3
- title: XLSX Import
4
- description: Drop an .xlsx file into a fully featured OGrid — with multi-sheet tabs, formulas, and cell references on by default
3
+ title: XLSX Import & Export
4
+ description: Drop an .xlsx file into a fully featured OGrid — and export grid data back out as a real workbook
5
5
  ---
6
6
 
7
7
 
8
- # XLSX Import
9
8
 
10
- Render any `.xlsx`, CSV, or TSV blob as a fully featured OGrid. Multi-sheet workbooks get tabbed navigation, formulas evaluate live, and Excel-style cell references (A1, B2…) are on by default.
9
+ # XLSX Import & Export
10
+
11
+ Render any `.xlsx`, CSV, or TSV blob as a fully featured OGrid — and export grid data back out as a real `.xlsx` workbook. Multi-sheet workbooks get tabbed navigation, formulas evaluate live, and Excel-style cell references (A1, B2…) are on by default.
12
+
13
+ <XlsxImportDemo />
11
14
 
12
15
  Two packages ship this:
13
16
 
@@ -64,7 +67,7 @@ function SingleSheet({ file }: { file: Blob }) {
64
67
 
65
68
  ### Format support
66
69
 
67
- ExcelJS-backed: `.xlsx`, `.csv`, `.tsv`. The `xlsx` package on npm (SheetJS) is currently stuck at the vulnerable 0.18.5, so support for `.xls`, `.xlsm`, `.xlsb`, and `.ods` was dropped in 2.12.0. See CHANGELOG for the swap rationale.
70
+ Supported formats: `.xlsx`, `.csv`, `.tsv` (backed by ExcelJS).
68
71
 
69
72
  ## Browser bundle (no bundler)
70
73
 
@@ -117,6 +120,34 @@ Returns an unmount function. Always call it before removing the host node — Re
117
120
  | You want tree-shaking and a single dependency graph. | You don't have (and don't want) a JS bundler. |
118
121
  | You're integrating into an existing React tree. | You want one `<script type="module">` import. |
119
122
 
123
+ ## Exporting
124
+
125
+ `exportToXlsx` mirrors [`exportToCsv`](./csv-export)'s call shape — pass rows, lightweight column defs, a value accessor, and a filename:
126
+
127
+ ```tsx
128
+
129
+ await exportToXlsx(
130
+ rows,
131
+ [
132
+ { columnId: 'name', name: 'Name' },
133
+ { columnId: 'salary', name: 'Salary' },
134
+ ],
135
+ (item, columnId) => item[columnId],
136
+ 'people.xlsx',
137
+ );
138
+ ```
139
+
140
+ Values export as native Excel types: numbers, dates, and booleans round-trip without stringification. To emit **live formulas** (Excel recalculates them on open), pass the same `{col, row, formula}` array `sheetToGridData` produced on import:
141
+
142
+ ```tsx
143
+ await exportToXlsx(rows, columns, getValue, 'sheet.xlsx', {
144
+ sheetName: 'Data',
145
+ formulas: initialFormulas, // e.g. from the imported workbook
146
+ });
147
+ ```
148
+
149
+ For full control (multi-sheet assembly, styling), build the workbook yourself with `workbookFromGridData(rows, columns, getValue, options)` — it returns an `ExcelJS.Workbook` you can add sheets to before serializing with `xlsxBlobFromWorkbook(wb)`.
150
+
120
151
  ## Related
121
152
 
122
153
  - [Formulas](./formulas) — the engine that evaluates `=SUM(A1:A5)` cells lifted out of the workbook
@@ -81,10 +81,10 @@ The full integration code is the
81
81
 
82
82
  ## Both at once — yes, in the same app
83
83
 
84
- Mix freely. A typical pattern:
84
+ Mix freely. A common pattern:
85
85
 
86
- - 80% of admin / list pages → `<OGrid>` (fast to build, good enough chrome)
87
- - 20% of "the data table is the product" pages headless hooks (custom chrome wins)
86
+ - Standard admin / list pages → `<OGrid>` (fast to build, sensible default UI)
87
+ - Pages where the table is the product → the hooks (full control over the markup)
88
88
 
89
89
  Same package import. Same theme tokens. Same TypeScript types. The
90
90
  spreadsheet features (inline edit, fill handle, clipboard, undo) work
@@ -34,14 +34,9 @@ npm install @alaarab/ogrid-react-fluent @fluentui/react-components
34
34
 
35
35
  ---
36
36
 
37
- ## Frozen adapters
37
+ ## Other adapters
38
38
 
39
- The following adapters remain on npm at their last shipped version but no longer receive feature work or framework-major upgrades. Existing installs continue to work unchanged. Source for each lives on the `legacy/multiframework` branch of the repo.
40
-
41
- - **`@alaarab/ogrid-react-material`** — frozen at v2.9.1. MUI v7 (skipped v8, broke in v9).
42
- - **`@alaarab/ogrid-js`** + **`@alaarab/ogrid-js-inputs`** — frozen at v2.9.1. The vanilla JS variant.
43
- - **`@alaarab/ogrid-angular*`** — frozen at v2.9.0.
44
- - **`@alaarab/ogrid-vue*`** — frozen at v2.9.0.
39
+ OGrid is React-first (Radix or Fluent). Material UI, vanilla JS, Angular, and Vue adapters were published earlier and remain on npm for existing installs, but are no longer actively developed.
45
40
 
46
41
  ---
47
42
 
@@ -8,7 +8,7 @@ description: What is OGrid and why use it
8
8
 
9
9
  OGrid is a lightweight, open-source React data grid library that delivers spreadsheet-grade features without the enterprise paywall. It ships React UI implementations for Radix and Fluent UI, all powered by a shared TypeScript core.
10
10
 
11
- > **Note:** Angular, Vue, vanilla JS, and Material UI adapters were available through earlier 2.9.x versions and remain on npm at those versions. New development is React-first (Radix / Fluent). See the project README's "Frozen adapters" section if you have an existing install on one of the frozen variants.
11
+ > **Note:** OGrid is React-first (Radix or Fluent). Angular, Vue, vanilla JS, and Material UI adapters were published earlier and remain on npm for existing installs, but are no longer actively developed.
12
12
 
13
13
  ## Why OGrid?
14
14
 
@@ -187,7 +187,7 @@ Users can resize columns by dragging the column border. Listen for resize events
187
187
  />
188
188
  ```
189
189
 
190
- ## Theme presets (new in 2.9)
190
+ ## Theme presets
191
191
 
192
192
  Instead of mapping `--ogrid-*` variables to your design system one-by-one,
193
193
  shadcn/Tailwind v4 preset ships with `@alaarab/ogrid-react-radix`:
@@ -207,9 +207,9 @@ Both light and dark modes follow your shadcn theme automatically — no
207
207
  hand-authored `[data-theme="dark"]` override blocks needed. The `.dark`
208
208
  class on `<html>` (Tailwind v3+/shadcn convention) is fully supported.
209
209
 
210
- ### New theming tokens (2.9)
210
+ ### Theming tokens
211
211
 
212
- In addition to the colors documented above, 2.8.0 added a radius/font/ring
212
+ In addition to the colors documented above, a radius/font/ring
213
213
  scale that `preset-shadcn.css` bridges automatically:
214
214
 
215
215
  | Variable | Default | Description |
@@ -63,9 +63,9 @@ function connectGridToBridge(options) {
63
63
  try {
64
64
  switch (cmd.type) {
65
65
  case "update_cell": {
66
- const rowIndex = cmd.payload["rowIndex"];
67
- const columnId = cmd.payload["columnId"];
68
- const value = cmd.payload["value"];
66
+ const rowIndex = cmd.payload.rowIndex;
67
+ const columnId = cmd.payload.columnId;
68
+ const value = cmd.payload.value;
69
69
  if (onCellUpdate) {
70
70
  onCellUpdate(rowIndex, columnId, value);
71
71
  result = { ok: true, rowIndex, columnId, value };
@@ -75,8 +75,8 @@ function connectGridToBridge(options) {
75
75
  break;
76
76
  }
77
77
  case "set_filter": {
78
- const columnId = cmd.payload["columnId"];
79
- const value = cmd.payload["value"];
78
+ const columnId = cmd.payload.columnId;
79
+ const value = cmd.payload.value;
80
80
  if (api?.updateFilter) {
81
81
  api.updateFilter(columnId, value);
82
82
  result = { ok: true };
@@ -95,7 +95,7 @@ function connectGridToBridge(options) {
95
95
  break;
96
96
  }
97
97
  case "set_sort": {
98
- const sortModel = cmd.payload["sortModel"];
98
+ const sortModel = cmd.payload.sortModel;
99
99
  if (api?.updateSort) {
100
100
  api.updateSort(sortModel);
101
101
  result = { ok: true };
@@ -105,7 +105,7 @@ function connectGridToBridge(options) {
105
105
  break;
106
106
  }
107
107
  case "go_to_page": {
108
- const page = cmd.payload["page"];
108
+ const page = cmd.payload.page;
109
109
  if (api?.goToPage) {
110
110
  api.goToPage(page);
111
111
  result = { ok: true };
package/dist/esm/index.js CHANGED
@@ -27,7 +27,7 @@ var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---/;
27
27
  function parseFrontmatter(raw) {
28
28
  const match = FRONTMATTER_RE.exec(raw);
29
29
  if (!match) return { title: "", description: "" };
30
- const block = match[1];
30
+ const block = match[1] ?? "";
31
31
  let title = "";
32
32
  let description = "";
33
33
  for (const line of block.split("\n")) {
@@ -56,7 +56,7 @@ function extractCodeBlocks(raw) {
56
56
  CODE_BLOCK_RE.lastIndex = 0;
57
57
  while ((match = CODE_BLOCK_RE.exec(raw)) !== null) {
58
58
  const language = match[1] || "text";
59
- const code = match[2].trim();
59
+ const code = (match[2] ?? "").trim();
60
60
  const precedingStart = Math.max(0, match.index - 300);
61
61
  const surroundingContext = raw.slice(precedingStart, match.index);
62
62
  const framework = detectFramework(language, code, surroundingContext);
@@ -218,14 +218,15 @@ function detectOGridVersion(searchPath) {
218
218
  const raw = readFileSync(pkgPath, "utf-8");
219
219
  const pkg = JSON.parse(raw);
220
220
  const allDeps = {
221
- ...pkg["dependencies"] ?? {},
222
- ...pkg["devDependencies"] ?? {},
223
- ...pkg["peerDependencies"] ?? {}
221
+ ...pkg.dependencies ?? {},
222
+ ...pkg.devDependencies ?? {},
223
+ ...pkg.peerDependencies ?? {}
224
224
  };
225
225
  const ogridPkgs = Object.entries(allDeps).filter(([name]) => name.startsWith("@alaarab/ogrid-")).map(([name, version]) => ({ name, version: String(version) }));
226
- if (ogridPkgs.length > 0) {
226
+ const firstPkg = ogridPkgs[0];
227
+ if (firstPkg) {
227
228
  const framework = detectFramework2(ogridPkgs.map((p) => p.name));
228
- const version = ogridPkgs[0].version.replace(/^[\^~>=<]+/, "");
229
+ const version = firstPkg.version.replace(/^[\^~>=<]+/, "");
229
230
  return { found: true, version, framework, packages: ogridPkgs, packageJsonPath: pkgPath };
230
231
  }
231
232
  } catch {
@@ -280,9 +281,10 @@ Categories: features, getting-started, guides, api.`
280
281
  const formatted = results.map((entry, i) => {
281
282
  const excerpt = entry.content.length > 400 ? entry.content.slice(0, 400) + "..." : entry.content;
282
283
  const relevantCode = framework ? entry.codeBlocks.filter((b) => !b.framework || b.framework === framework).slice(0, 1) : [];
283
- const codeSnippet = relevantCode.length > 0 ? `
284
- \`\`\`${relevantCode[0].language}
285
- ${relevantCode[0].code.slice(0, 300)}
284
+ const firstCode = relevantCode[0];
285
+ const codeSnippet = firstCode ? `
286
+ \`\`\`${firstCode.language}
287
+ ${firstCode.code.slice(0, 300)}
286
288
  \`\`\`` : "";
287
289
  return [
288
290
  `## ${i + 1}. ${entry.title}`,
@@ -537,6 +539,7 @@ Tip: use \`get_code_example\` with framework="${result.framework}" or \`search_d
537
539
  "```tsx",
538
540
  "const dataSource = {",
539
541
  " fetchPage: async ({ page, pageSize, sort, filters, signal }) => {",
542
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: intentional — this is documentation example code emitted as text; the ${} placeholders belong to the sample snippet
540
543
  " const res = await fetch(`/api/data?page=${page}&size=${pageSize}`, { signal });",
541
544
  " const json = await res.json();",
542
545
  " return { items: json.data, totalCount: json.total };",
@@ -964,26 +967,37 @@ function readBody(req) {
964
967
  req.on("error", reject);
965
968
  });
966
969
  }
967
- function send(res, status, body) {
970
+ var LOCALHOST_ORIGIN = /^https?:\/\/(localhost|127\.0\.0\.1|\[::1\])(:\d+)?$/;
971
+ function corsOrigin(req) {
972
+ const origin = req.headers.origin;
973
+ return origin && LOCALHOST_ORIGIN.test(origin) ? origin : "";
974
+ }
975
+ function send(req, res, status, body) {
968
976
  const json = JSON.stringify(body);
969
- res.writeHead(status, {
977
+ const headers = {
970
978
  "Content-Type": "application/json",
971
979
  "Content-Length": Buffer.byteLength(json),
972
- "Access-Control-Allow-Origin": "*",
980
+ Vary: "Origin",
973
981
  "Access-Control-Allow-Methods": "GET, POST, PUT, OPTIONS",
974
982
  "Access-Control-Allow-Headers": "Content-Type"
975
- });
983
+ };
984
+ const allow = corsOrigin(req);
985
+ if (allow) headers["Access-Control-Allow-Origin"] = allow;
986
+ res.writeHead(status, headers);
976
987
  res.end(json);
977
988
  }
978
989
  function startBridgeServer(store, port = 7890) {
979
990
  return new Promise((resolve, reject) => {
980
991
  const httpServer = createServer(async (req, res) => {
981
992
  if (req.method === "OPTIONS") {
982
- res.writeHead(204, {
983
- "Access-Control-Allow-Origin": "*",
993
+ const headers = {
984
994
  "Access-Control-Allow-Methods": "GET, POST, PUT, OPTIONS",
985
- "Access-Control-Allow-Headers": "Content-Type"
986
- });
995
+ "Access-Control-Allow-Headers": "Content-Type",
996
+ Vary: "Origin"
997
+ };
998
+ const allow = corsOrigin(req);
999
+ if (allow) headers["Access-Control-Allow-Origin"] = allow;
1000
+ res.writeHead(204, headers);
987
1001
  res.end();
988
1002
  return;
989
1003
  }
@@ -991,45 +1005,45 @@ function startBridgeServer(store, port = 7890) {
991
1005
  const parts = url.pathname.replace(/^\//, "").split("/");
992
1006
  try {
993
1007
  if (req.method === "GET" && parts[0] === "health") {
994
- send(res, 200, { ok: true, grids: store.listGrids().length });
1008
+ send(req, res, 200, { ok: true, grids: store.listGrids().length });
995
1009
  return;
996
1010
  }
997
1011
  if (req.method === "POST" && parts[0] === "grids" && parts[1] === "connect") {
998
1012
  const body = await readBody(req);
999
- const gridId = String(body?.["gridId"] ?? "");
1013
+ const gridId = String(body?.gridId ?? "");
1000
1014
  if (!gridId) {
1001
- send(res, 400, { error: "gridId required" });
1015
+ send(req, res, 400, { error: "gridId required" });
1002
1016
  return;
1003
1017
  }
1004
1018
  store.upsertGrid(gridId, body);
1005
- send(res, 200, { ok: true });
1019
+ send(req, res, 200, { ok: true });
1006
1020
  return;
1007
1021
  }
1008
- if (req.method === "PUT" && parts[0] === "grids" && parts[2] === "state") {
1022
+ if (req.method === "PUT" && parts[0] === "grids" && parts[1] != null && parts[2] === "state") {
1009
1023
  const gridId = parts[1];
1010
1024
  const body = await readBody(req);
1011
1025
  store.upsertGrid(gridId, body);
1012
- send(res, 200, { ok: true });
1026
+ send(req, res, 200, { ok: true });
1013
1027
  return;
1014
1028
  }
1015
- if (req.method === "GET" && parts[0] === "grids" && parts[2] === "commands") {
1029
+ if (req.method === "GET" && parts[0] === "grids" && parts[1] != null && parts[2] === "commands") {
1016
1030
  const gridId = parts[1];
1017
1031
  const state = store.getState(gridId);
1018
1032
  if (state) store.upsertGrid(gridId, {});
1019
1033
  const cmds = store.popPendingCommands(gridId);
1020
- send(res, 200, cmds);
1034
+ send(req, res, 200, cmds);
1021
1035
  return;
1022
1036
  }
1023
- if (req.method === "POST" && parts[0] === "grids" && parts[2] === "commands" && parts[4] === "result") {
1037
+ if (req.method === "POST" && parts[0] === "grids" && parts[2] === "commands" && parts[3] != null && parts[4] === "result") {
1024
1038
  const cmdId = parts[3];
1025
1039
  const body = await readBody(req);
1026
- store.resolveCommand(cmdId, body?.["result"], body?.["error"]);
1027
- send(res, 200, { ok: true });
1040
+ store.resolveCommand(cmdId, body?.result, body?.error);
1041
+ send(req, res, 200, { ok: true });
1028
1042
  return;
1029
1043
  }
1030
- send(res, 404, { error: "Not found" });
1044
+ send(req, res, 404, { error: "Not found" });
1031
1045
  } catch (err) {
1032
- send(res, 500, { error: String(err) });
1046
+ send(req, res, 500, { error: String(err) });
1033
1047
  }
1034
1048
  });
1035
1049
  httpServer.on("error", reject);
@@ -1053,10 +1067,10 @@ if (process.argv.includes("--version") || process.argv.includes("-v")) {
1053
1067
  var __dirname$1 = dirname(fileURLToPath(import.meta.url));
1054
1068
  var monorepoDocs = join(__dirname$1, "../../../docs/docs");
1055
1069
  var bundledDocs = join(__dirname$1, "../../bundled-docs");
1056
- var docsDir = process.env["OGRID_DOCS_PATH"] ?? (existsSync(monorepoDocs) ? monorepoDocs : bundledDocs);
1070
+ var docsDir = process.env.OGRID_DOCS_PATH ?? (existsSync(monorepoDocs) ? monorepoDocs : bundledDocs);
1057
1071
  var index = loadDocsIndex(docsDir);
1058
1072
  var bridgeStore = new BridgeStore();
1059
- var bridgePort = process.env["OGRID_BRIDGE_PORT"] ? parseInt(process.env["OGRID_BRIDGE_PORT"], 10) : process.argv.includes("--bridge") ? 7890 : null;
1073
+ var bridgePort = process.env.OGRID_BRIDGE_PORT ? parseInt(process.env.OGRID_BRIDGE_PORT, 10) : process.argv.includes("--bridge") ? 7890 : null;
1060
1074
  if (bridgePort !== null) {
1061
1075
  try {
1062
1076
  await startBridgeServer(bridgeStore, bridgePort);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alaarab/ogrid-mcp",
3
- "version": "2.15.1",
3
+ "version": "2.15.2",
4
4
  "description": "MCP server for OGrid documentation",
5
5
  "type": "module",
6
6
  "repository": {