@alaarab/ogrid-mcp 2.4.0

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.
Files changed (52) hide show
  1. package/README.md +68 -0
  2. package/bundled-docs/api/README.md +94 -0
  3. package/bundled-docs/api/column-def.mdx +379 -0
  4. package/bundled-docs/api/components-column-chooser.mdx +310 -0
  5. package/bundled-docs/api/components-column-header-filter.mdx +363 -0
  6. package/bundled-docs/api/components-datagrid-table.mdx +316 -0
  7. package/bundled-docs/api/components-pagination-controls.mdx +344 -0
  8. package/bundled-docs/api/components-sidebar.mdx +427 -0
  9. package/bundled-docs/api/components-status-bar.mdx +309 -0
  10. package/bundled-docs/api/grid-api.mdx +299 -0
  11. package/bundled-docs/api/js-api.mdx +198 -0
  12. package/bundled-docs/api/ogrid-props.mdx +244 -0
  13. package/bundled-docs/api/types.mdx +640 -0
  14. package/bundled-docs/features/cell-references.mdx +225 -0
  15. package/bundled-docs/features/column-chooser.mdx +279 -0
  16. package/bundled-docs/features/column-groups.mdx +290 -0
  17. package/bundled-docs/features/column-pinning.mdx +282 -0
  18. package/bundled-docs/features/column-reordering.mdx +359 -0
  19. package/bundled-docs/features/column-types.mdx +181 -0
  20. package/bundled-docs/features/context-menu.mdx +216 -0
  21. package/bundled-docs/features/csv-export.mdx +227 -0
  22. package/bundled-docs/features/editing.mdx +377 -0
  23. package/bundled-docs/features/filtering.mdx +330 -0
  24. package/bundled-docs/features/formulas.mdx +381 -0
  25. package/bundled-docs/features/grid-api.mdx +311 -0
  26. package/bundled-docs/features/keyboard-navigation.mdx +236 -0
  27. package/bundled-docs/features/pagination.mdx +245 -0
  28. package/bundled-docs/features/performance.mdx +433 -0
  29. package/bundled-docs/features/row-selection.mdx +256 -0
  30. package/bundled-docs/features/server-side-data.mdx +291 -0
  31. package/bundled-docs/features/sidebar.mdx +234 -0
  32. package/bundled-docs/features/sorting.mdx +241 -0
  33. package/bundled-docs/features/spreadsheet-selection.mdx +201 -0
  34. package/bundled-docs/features/status-bar.mdx +205 -0
  35. package/bundled-docs/features/toolbar.mdx +284 -0
  36. package/bundled-docs/features/virtual-scrolling.mdx +624 -0
  37. package/bundled-docs/getting-started/installation.mdx +216 -0
  38. package/bundled-docs/getting-started/overview.mdx +151 -0
  39. package/bundled-docs/getting-started/quick-start.mdx +425 -0
  40. package/bundled-docs/getting-started/vanilla-js.mdx +191 -0
  41. package/bundled-docs/guides/accessibility.mdx +550 -0
  42. package/bundled-docs/guides/controlled-vs-uncontrolled.mdx +153 -0
  43. package/bundled-docs/guides/custom-cell-editors.mdx +201 -0
  44. package/bundled-docs/guides/framework-showcase.mdx +200 -0
  45. package/bundled-docs/guides/mcp-live-testing.mdx +291 -0
  46. package/bundled-docs/guides/mcp.mdx +172 -0
  47. package/bundled-docs/guides/migration-from-ag-grid.mdx +223 -0
  48. package/bundled-docs/guides/theming.mdx +211 -0
  49. package/dist/esm/bridge-client.d.ts +87 -0
  50. package/dist/esm/bridge-client.js +162 -0
  51. package/dist/esm/index.js +1060 -0
  52. package/package.json +43 -0
@@ -0,0 +1,1060 @@
1
+ #!/usr/bin/env node
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { readFileSync, existsSync, statSync, readdirSync } from 'fs';
4
+ import { join, dirname, extname, relative } from 'path';
5
+ import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
6
+ import { z } from 'zod';
7
+ import { createServer } from 'http';
8
+ import { fileURLToPath } from 'url';
9
+
10
+ function collectFiles(dir) {
11
+ const results = [];
12
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
13
+ const full = join(dir, entry.name);
14
+ if (entry.isDirectory()) {
15
+ results.push(...collectFiles(full));
16
+ } else if (entry.isFile() && (extname(entry.name) === ".mdx" || extname(entry.name) === ".md")) {
17
+ results.push(full);
18
+ }
19
+ }
20
+ return results;
21
+ }
22
+ function deriveCategory(relPath) {
23
+ const first = relPath.split("/")[0];
24
+ return first ?? "uncategorized";
25
+ }
26
+ var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---/;
27
+ function parseFrontmatter(raw) {
28
+ const match = FRONTMATTER_RE.exec(raw);
29
+ if (!match) return { title: "", description: "" };
30
+ const block = match[1];
31
+ let title = "";
32
+ let description = "";
33
+ for (const line of block.split("\n")) {
34
+ const trimmed = line.trim();
35
+ if (trimmed.startsWith("title:")) {
36
+ title = trimmed.slice("title:".length).trim().replace(/^['"]|['"]$/g, "");
37
+ } else if (trimmed.startsWith("description:")) {
38
+ description = trimmed.slice("description:".length).trim().replace(/^['"]|['"]$/g, "");
39
+ }
40
+ }
41
+ return { title, description };
42
+ }
43
+ var CODE_BLOCK_RE = /```(\w*)\n([\s\S]*?)```/g;
44
+ function detectFramework(language, code, surroundingContext) {
45
+ if (language === "tsx" || language === "jsx") return "react";
46
+ if (language === "vue") return "vue";
47
+ const ctxLower = surroundingContext.toLowerCase();
48
+ if (ctxLower.includes('value="react"') || ctxLower.includes('label="react"'))
49
+ return "react";
50
+ if (ctxLower.includes('value="angular"') || ctxLower.includes('label="angular"'))
51
+ return "angular";
52
+ if (ctxLower.includes('value="vue"') || ctxLower.includes('label="vue"'))
53
+ return "vue";
54
+ if (ctxLower.includes('value="js"') || ctxLower.includes('label="vanilla js"') || ctxLower.includes("vanilla js"))
55
+ return "js";
56
+ if (code.includes("@angular/") || code.includes("@Component")) return "angular";
57
+ if (code.includes("ogrid-vue") || code.includes("<template>")) return "vue";
58
+ if (code.includes("ogrid-react") || code.includes("from 'react'"))
59
+ return "react";
60
+ if (code.includes("ogrid-js") || code.includes("new OGrid(")) return "js";
61
+ return void 0;
62
+ }
63
+ function extractCodeBlocks(raw) {
64
+ const blocks = [];
65
+ let match;
66
+ CODE_BLOCK_RE.lastIndex = 0;
67
+ while ((match = CODE_BLOCK_RE.exec(raw)) !== null) {
68
+ const language = match[1] || "text";
69
+ const code = match[2].trim();
70
+ const precedingStart = Math.max(0, match.index - 300);
71
+ const surroundingContext = raw.slice(precedingStart, match.index);
72
+ const framework = detectFramework(language, code, surroundingContext);
73
+ blocks.push({ language, code, framework });
74
+ }
75
+ return blocks;
76
+ }
77
+ function stripMdxContent(raw) {
78
+ let text = raw;
79
+ text = text.replace(FRONTMATTER_RE, "");
80
+ text = text.replace(/^import\s+.*?;\s*$/gm, "");
81
+ text = text.replace(/```\w*\n[\s\S]*?```/g, "");
82
+ text = text.replace(/<[A-Z]\w*\s*\/>/g, "");
83
+ text = text.replace(/<\/?[A-Z][\w.]*(?:\s[^>]*)?>/g, "");
84
+ text = text.replace(/<\/?[a-z][\w-]*(?:\s[^>]*)?\/?\s*>/g, "");
85
+ text = text.replace(/^:::\w+.*$/gm, "");
86
+ text = text.replace(/^:::$/gm, "");
87
+ text = text.replace(/\n{3,}/g, "\n\n");
88
+ return text.trim();
89
+ }
90
+ function parseDocFile(filePath, docsDir2) {
91
+ const raw = readFileSync(filePath, "utf-8");
92
+ const relPath = relative(docsDir2, filePath);
93
+ const { title, description } = parseFrontmatter(raw);
94
+ const category = deriveCategory(relPath);
95
+ const content = stripMdxContent(raw);
96
+ const codeBlocks = extractCodeBlocks(raw);
97
+ return {
98
+ path: relPath,
99
+ title: title || relPath,
100
+ description: description || "",
101
+ category,
102
+ content,
103
+ codeBlocks
104
+ };
105
+ }
106
+ function scoreEntry(entry, queryLower) {
107
+ let score = 0;
108
+ const titleLower = entry.title.toLowerCase();
109
+ const descLower = entry.description.toLowerCase();
110
+ const contentLower = entry.content.toLowerCase();
111
+ if (titleLower.includes(queryLower)) {
112
+ score += 100;
113
+ if (titleLower === queryLower) score += 50;
114
+ if (titleLower.startsWith(queryLower)) score += 25;
115
+ }
116
+ if (descLower.includes(queryLower)) {
117
+ score += 50;
118
+ }
119
+ if (contentLower.includes(queryLower)) {
120
+ score += 10;
121
+ let idx = 0;
122
+ let count = 0;
123
+ while (count < 10) {
124
+ idx = contentLower.indexOf(queryLower, idx);
125
+ if (idx === -1) break;
126
+ count++;
127
+ idx += queryLower.length;
128
+ }
129
+ score += count * 2;
130
+ }
131
+ if (entry.category === "features") score += 3;
132
+ if (entry.category === "getting-started") score += 2;
133
+ return score;
134
+ }
135
+ function scoreCodeBlock(entry, block, queryLower, framework) {
136
+ let score = 0;
137
+ if (framework && block.framework !== framework) return -1;
138
+ if (entry.title.toLowerCase().includes(queryLower)) score += 50;
139
+ if (entry.description.toLowerCase().includes(queryLower)) score += 25;
140
+ if (block.code.toLowerCase().includes(queryLower)) score += 30;
141
+ return score;
142
+ }
143
+ function loadDocsIndex(docsDir2) {
144
+ try {
145
+ statSync(docsDir2);
146
+ } catch {
147
+ throw new Error(`Docs directory not found: ${docsDir2}`);
148
+ }
149
+ const files = collectFiles(docsDir2);
150
+ const entries = files.map((f) => parseDocFile(f, docsDir2));
151
+ const pathMap = /* @__PURE__ */ new Map();
152
+ for (const entry of entries) {
153
+ pathMap.set(entry.path, entry);
154
+ }
155
+ return {
156
+ entries,
157
+ search(query, limit = 5) {
158
+ const q = query.toLowerCase().trim();
159
+ if (!q) return [];
160
+ const scored = entries.map((entry) => ({ entry, score: scoreEntry(entry, q) })).filter((r) => r.score > 0).sort((a, b) => b.score - a.score);
161
+ return scored.slice(0, limit).map((r) => r.entry);
162
+ },
163
+ getByPath(path) {
164
+ return pathMap.get(path);
165
+ },
166
+ getByCategory(category) {
167
+ return entries.filter((e) => e.category === category);
168
+ },
169
+ getCodeExamples(query, framework) {
170
+ const q = query.toLowerCase().trim();
171
+ if (!q) return [];
172
+ const results = [];
173
+ for (const entry of entries) {
174
+ for (const block of entry.codeBlocks) {
175
+ const s = scoreCodeBlock(entry, block, q, framework);
176
+ if (s > 0) {
177
+ results.push({ entry, block, score: s });
178
+ }
179
+ }
180
+ }
181
+ results.sort((a, b) => b.score - a.score);
182
+ return results.slice(0, 10).map(({ entry, block }) => ({ entry, block }));
183
+ }
184
+ };
185
+ }
186
+ function detectFramework2(packageNames) {
187
+ if (packageNames.some((n) => n.includes("-react"))) return "react";
188
+ if (packageNames.some((n) => n.includes("-angular"))) return "angular";
189
+ if (packageNames.some((n) => n.includes("-vue"))) return "vue";
190
+ if (packageNames.some((n) => n.endsWith("-js"))) return "js";
191
+ return "unknown";
192
+ }
193
+ function detectOGridVersion(searchPath) {
194
+ let dir = searchPath;
195
+ for (let i = 0; i < 10; i++) {
196
+ const pkgPath = join(dir, "package.json");
197
+ if (existsSync(pkgPath)) {
198
+ try {
199
+ const raw = readFileSync(pkgPath, "utf-8");
200
+ const pkg = JSON.parse(raw);
201
+ const allDeps = {
202
+ ...pkg["dependencies"] ?? {},
203
+ ...pkg["devDependencies"] ?? {},
204
+ ...pkg["peerDependencies"] ?? {}
205
+ };
206
+ const ogridPkgs = Object.entries(allDeps).filter(([name]) => name.startsWith("@alaarab/ogrid-")).map(([name, version]) => ({ name, version: String(version) }));
207
+ if (ogridPkgs.length > 0) {
208
+ const framework = detectFramework2(ogridPkgs.map((p) => p.name));
209
+ const version = ogridPkgs[0].version.replace(/^[\^~>=<]+/, "");
210
+ return { found: true, version, framework, packages: ogridPkgs, packageJsonPath: pkgPath };
211
+ }
212
+ } catch {
213
+ }
214
+ }
215
+ const parent = dirname(dir);
216
+ if (parent === dir) break;
217
+ dir = parent;
218
+ }
219
+ return { found: false };
220
+ }
221
+ function toResourcePath(docPath) {
222
+ return docPath.replace(/\.(mdx|md)$/, "");
223
+ }
224
+ function fromResourcePath(resourcePath, index2) {
225
+ return index2.getByPath(resourcePath + ".mdx") ?? index2.getByPath(resourcePath + ".md") ?? index2.getByPath(resourcePath);
226
+ }
227
+ function createOGridMcpServer(index2, bridge) {
228
+ const server2 = new McpServer({
229
+ name: "ogrid-docs",
230
+ version: "2.3.0",
231
+ instructions: `OGrid documentation server. OGrid is a lightweight multi-framework data grid for React, Angular, Vue, and vanilla JS.
232
+
233
+ Tools: search_docs (keyword search), list_docs (browse by category), get_docs (full page), get_code_example (code snippets), detect_version (detect OGrid version in your project).
234
+ Resources: ogrid://quick-reference (key API overview), ogrid://docs/{path} (any doc page by path).
235
+ Categories: features, getting-started, guides, api.`
236
+ });
237
+ server2.tool(
238
+ "search_docs",
239
+ "Search OGrid documentation by keyword. Returns matching docs with title, description, and content excerpt.",
240
+ {
241
+ query: z.string().describe("Search query string"),
242
+ limit: z.number().int().min(1).max(20).optional().describe("Max results to return (default 5)"),
243
+ framework: z.enum(["react", "angular", "vue", "js"]).optional().describe("Filter code examples to this framework")
244
+ },
245
+ async ({ query, limit, framework }) => {
246
+ const results = index2.search(query, limit ?? 5);
247
+ if (results.length === 0) {
248
+ return {
249
+ content: [
250
+ {
251
+ type: "text",
252
+ text: `No documentation found for "${query}". Try a different search term or use list_docs to browse available docs.`
253
+ }
254
+ ]
255
+ };
256
+ }
257
+ const formatted = results.map((entry, i) => {
258
+ const excerpt = entry.content.length > 400 ? entry.content.slice(0, 400) + "..." : entry.content;
259
+ const relevantCode = framework ? entry.codeBlocks.filter((b) => !b.framework || b.framework === framework).slice(0, 1) : [];
260
+ const codeSnippet = relevantCode.length > 0 ? `
261
+ \`\`\`${relevantCode[0].language}
262
+ ${relevantCode[0].code.slice(0, 300)}
263
+ \`\`\`` : "";
264
+ return [
265
+ `## ${i + 1}. ${entry.title}`,
266
+ `**Path:** ${entry.path} | **Category:** ${entry.category}`,
267
+ `**Description:** ${entry.description}`,
268
+ "",
269
+ excerpt,
270
+ codeSnippet
271
+ ].filter(Boolean).join("\n");
272
+ }).join("\n\n---\n\n");
273
+ return {
274
+ content: [
275
+ {
276
+ type: "text",
277
+ text: `Found ${results.length} result(s) for "${query}":
278
+
279
+ ${formatted}`
280
+ }
281
+ ]
282
+ };
283
+ }
284
+ );
285
+ server2.tool(
286
+ "list_docs",
287
+ "List available OGrid documentation pages, optionally filtered by category (features, getting-started, guides, api).",
288
+ {
289
+ category: z.string().optional().describe("Filter by category: features, getting-started, guides, api")
290
+ },
291
+ async ({ category }) => {
292
+ const entries = category ? index2.getByCategory(category) : index2.entries;
293
+ if (entries.length === 0) {
294
+ const available = [...new Set(index2.entries.map((e) => e.category))];
295
+ return {
296
+ content: [
297
+ {
298
+ type: "text",
299
+ text: category ? `No docs found in category "${category}". Available categories: ${available.join(", ")}` : "No documentation entries found."
300
+ }
301
+ ]
302
+ };
303
+ }
304
+ const formatted = entries.map(
305
+ (entry) => `- **${entry.title}** \u2014 ${entry.description}
306
+ Path: \`${entry.path}\` | Resource: \`ogrid://docs/${toResourcePath(entry.path)}\``
307
+ ).join("\n");
308
+ const header = category ? `Documentation in "${category}" (${entries.length} pages):` : `All documentation (${entries.length} pages):`;
309
+ return {
310
+ content: [{ type: "text", text: `${header}
311
+
312
+ ${formatted}` }]
313
+ };
314
+ }
315
+ );
316
+ server2.tool(
317
+ "get_docs",
318
+ "Get the full content of an OGrid documentation page by its path.",
319
+ {
320
+ path: z.string().describe('Document path (e.g. "features/sorting" or "api/column-def")')
321
+ },
322
+ async ({ path }) => {
323
+ const entry = fromResourcePath(path, index2);
324
+ if (!entry) {
325
+ const available = index2.entries.map((e) => ` - ${toResourcePath(e.path)}`).join("\n");
326
+ return {
327
+ content: [
328
+ {
329
+ type: "text",
330
+ text: `Document not found: "${path}"
331
+
332
+ Available paths:
333
+ ${available}`
334
+ }
335
+ ]
336
+ };
337
+ }
338
+ const header = [
339
+ `# ${entry.title}`,
340
+ `**Category:** ${entry.category}`,
341
+ `**Description:** ${entry.description}`,
342
+ "",
343
+ "---",
344
+ ""
345
+ ].join("\n");
346
+ return {
347
+ content: [{ type: "text", text: header + entry.content }]
348
+ };
349
+ }
350
+ );
351
+ server2.tool(
352
+ "get_code_example",
353
+ "Find code examples from OGrid docs matching a query, optionally filtered by framework.",
354
+ {
355
+ query: z.string().describe("Search query for code examples"),
356
+ framework: z.enum(["react", "angular", "vue", "js"]).optional().describe("Filter by framework: react, angular, vue, js")
357
+ },
358
+ async ({ query, framework }) => {
359
+ const examples = index2.getCodeExamples(query, framework);
360
+ if (examples.length === 0) {
361
+ const hint = framework ? ` for framework "${framework}"` : "";
362
+ return {
363
+ content: [
364
+ {
365
+ type: "text",
366
+ text: `No code examples found matching "${query}"${hint}. Try a broader search term or remove the framework filter.`
367
+ }
368
+ ]
369
+ };
370
+ }
371
+ const limited = examples.slice(0, 5);
372
+ const formatted = limited.map((example, i) => {
373
+ const frameworkLabel = example.block.framework ? ` (${example.block.framework})` : "";
374
+ return [
375
+ `### Example ${i + 1}: ${example.entry.title}${frameworkLabel}`,
376
+ "",
377
+ "```" + example.block.language,
378
+ example.block.code,
379
+ "```"
380
+ ].join("\n");
381
+ }).join("\n\n");
382
+ return {
383
+ content: [
384
+ {
385
+ type: "text",
386
+ text: `Found ${examples.length} code example(s) for "${query}"${framework ? ` (${framework})` : ""}:
387
+
388
+ ${formatted}`
389
+ }
390
+ ]
391
+ };
392
+ }
393
+ );
394
+ server2.tool(
395
+ "detect_version",
396
+ "Detect which OGrid version and framework is installed in the user's project by reading their package.json.",
397
+ {
398
+ path: z.string().optional().describe(
399
+ "Directory to search from (defaults to current working directory). The tool walks up the directory tree to find the nearest package.json with OGrid dependencies."
400
+ )
401
+ },
402
+ async ({ path }) => {
403
+ const searchPath = path ?? process.cwd();
404
+ const result = detectOGridVersion(searchPath);
405
+ if (!result.found) {
406
+ return {
407
+ content: [
408
+ {
409
+ type: "text",
410
+ text: [
411
+ `No OGrid packages found in package.json (searched from: ${searchPath}).`,
412
+ "",
413
+ "Install OGrid for your framework:",
414
+ " React (Radix): npm install @alaarab/ogrid-react-radix",
415
+ " React (Material): npm install @alaarab/ogrid-react-material",
416
+ " React (Fluent): npm install @alaarab/ogrid-react-fluent",
417
+ " Angular Material: npm install @alaarab/ogrid-angular-material",
418
+ " Angular PrimeNG: npm install @alaarab/ogrid-angular-primeng",
419
+ " Vue Vuetify: npm install @alaarab/ogrid-vue-vuetify",
420
+ " Vue PrimeVue: npm install @alaarab/ogrid-vue-primevue",
421
+ " Vanilla JS: npm install @alaarab/ogrid-js"
422
+ ].join("\n")
423
+ }
424
+ ]
425
+ };
426
+ }
427
+ const pkgList = (result.packages ?? []).map((p) => ` - ${p.name}: ${p.version}`).join("\n");
428
+ const frameworkTip = result.framework !== "unknown" ? `
429
+
430
+ Tip: use \`get_code_example\` with framework="${result.framework}" or \`search_docs\` with framework="${result.framework}" to get framework-specific results.` : "";
431
+ return {
432
+ content: [
433
+ {
434
+ type: "text",
435
+ text: [
436
+ `\u2705 OGrid detected in ${result.packageJsonPath}`,
437
+ "",
438
+ `Version: ${result.version}`,
439
+ `Framework: ${result.framework}`,
440
+ "",
441
+ "Packages installed:",
442
+ pkgList,
443
+ frameworkTip
444
+ ].filter((l) => l !== void 0).join("\n")
445
+ }
446
+ ]
447
+ };
448
+ }
449
+ );
450
+ server2.resource(
451
+ "quick-reference",
452
+ "ogrid://quick-reference",
453
+ { description: "OGrid quick-reference: key props, install commands, and common patterns", mimeType: "text/markdown" },
454
+ async (uri) => ({
455
+ contents: [
456
+ {
457
+ uri: uri.href,
458
+ mimeType: "text/markdown",
459
+ text: [
460
+ "# OGrid Quick Reference",
461
+ "",
462
+ "## Install",
463
+ "```bash",
464
+ "# React (choose one)",
465
+ "npm install @alaarab/ogrid-react-radix",
466
+ "npm install @alaarab/ogrid-react-material",
467
+ "npm install @alaarab/ogrid-react-fluent",
468
+ "",
469
+ "# Angular (choose one)",
470
+ "npm install @alaarab/ogrid-angular-material",
471
+ "npm install @alaarab/ogrid-angular-primeng",
472
+ "npm install @alaarab/ogrid-angular-radix",
473
+ "",
474
+ "# Vue (choose one)",
475
+ "npm install @alaarab/ogrid-vue-vuetify",
476
+ "npm install @alaarab/ogrid-vue-primevue",
477
+ "npm install @alaarab/ogrid-vue-radix",
478
+ "",
479
+ "# Vanilla JS",
480
+ "npm install @alaarab/ogrid-js",
481
+ "```",
482
+ "",
483
+ "## Core Props (IOGridProps)",
484
+ "| Prop | Type | Description |",
485
+ "|------|------|-------------|",
486
+ "| `data` | `T[]` | Client-side row data |",
487
+ "| `columns` | `IColumnDef<T>[]` | Column definitions |",
488
+ "| `dataSource` | `IDataSource<T>` | Server-side data source |",
489
+ "| `pagination` | `boolean \\| number` | Enable pagination (number = page size) |",
490
+ '| `rowSelection` | `"single" \\| "multiple"` | Row selection mode |',
491
+ "| `formulas` | `boolean` | Enable formula engine (=SUM, =IF, etc.) |",
492
+ "| `cellReferences` | `boolean` | Excel-style A1 column headers + name box |",
493
+ '| `workerSort` | `boolean \\| "auto"` | Web Worker sort/filter |',
494
+ '| `columnChooser` | `boolean \\| "toolbar" \\| "sidebar"` | Column visibility control |',
495
+ "| `sideBar` | `boolean \\| ISideBarDef` | Sidebar panel |",
496
+ "| `toolbar` | `ReactNode` | Custom toolbar content |",
497
+ "| `onRowSelectionChanged` | `(rows: T[]) => void` | Row selection callback |",
498
+ "| `onCellValueChanged` | `(e: ICellValueChangedEvent) => void` | Cell edit callback |",
499
+ "",
500
+ "## IColumnDef Key Fields",
501
+ "| Field | Type | Description |",
502
+ "|-------|------|-------------|",
503
+ "| `columnId` | `string` | Unique column ID (maps to data key) |",
504
+ "| `headerName` | `string` | Column header label |",
505
+ '| `type` | `"text" \\| "numeric" \\| "date" \\| "boolean"` | Data type |',
506
+ '| `filter` | `"none" \\| "text" \\| "multiSelect" \\| "date"` | Filter type |',
507
+ "| `editable` | `boolean \\| (row) => boolean` | Enable inline editing |",
508
+ "| `width` | `number` | Column width in px |",
509
+ '| `pinned` | `"left" \\| "right"` | Pin column |',
510
+ "| `sortable` | `boolean` | Enable sorting |",
511
+ "| `hidden` | `boolean` | Hide column by default |",
512
+ "| `valueGetter` | `(row: T) => unknown` | Custom value extractor |",
513
+ "| `renderCell` | `(value, row) => ReactNode` | Custom cell renderer (React) |",
514
+ "",
515
+ "## Common Patterns",
516
+ "",
517
+ "### Client-side data",
518
+ "```tsx",
519
+ 'import { OGrid } from "@alaarab/ogrid-react-radix";',
520
+ "const columns = [",
521
+ ' { columnId: "name", headerName: "Name", type: "text", filter: "text" },',
522
+ ' { columnId: "age", headerName: "Age", type: "numeric", sortable: true },',
523
+ "];",
524
+ "<OGrid data={rows} columns={columns} pagination={50} />",
525
+ "```",
526
+ "",
527
+ "### Server-side data",
528
+ "```tsx",
529
+ "const dataSource = {",
530
+ " fetchPage: async ({ page, pageSize, sortModel, filterModel }) => {",
531
+ " const res = await fetch(`/api/data?page=${page}&size=${pageSize}`);",
532
+ " const json = await res.json();",
533
+ " return { rows: json.data, totalCount: json.total };",
534
+ " }",
535
+ "};",
536
+ "<OGrid dataSource={dataSource} columns={columns} pagination={50} />",
537
+ "```",
538
+ "",
539
+ "### Formula support",
540
+ "```tsx",
541
+ "<OGrid data={rows} columns={columns} formulas cellReferences />",
542
+ '// Users can type =SUM(A1:C3), =IF(A1>0,"yes","no"), etc.',
543
+ "```"
544
+ ].join("\n")
545
+ }
546
+ ]
547
+ })
548
+ );
549
+ server2.resource(
550
+ "migration-guide",
551
+ "ogrid://migration-guide",
552
+ { description: "Full migration guide from AG Grid to OGrid with side-by-side API mapping", mimeType: "text/markdown" },
553
+ async (uri) => {
554
+ const entry = index2.getByPath("guides/migration-from-ag-grid.mdx") ?? index2.getByPath("guides/migration-from-ag-grid.md") ?? index2.getByPath("guides/migration-from-ag-grid");
555
+ const text = entry ? `# ${entry.title}
556
+ > ${entry.description}
557
+
558
+ ${entry.content}` : '# Migration from AG Grid\n\nMigration guide not found in docs index. Use `search_docs` with query "migration" to find available migration content.';
559
+ return {
560
+ contents: [
561
+ {
562
+ uri: uri.href,
563
+ mimeType: "text/markdown",
564
+ text
565
+ }
566
+ ]
567
+ };
568
+ }
569
+ );
570
+ server2.prompt(
571
+ "migrate-from-ag-grid",
572
+ "Step-by-step guide to migrate from AG Grid to OGrid",
573
+ async () => {
574
+ const entry = index2.getByPath("guides/migration-from-ag-grid.mdx") ?? index2.getByPath("guides/migration-from-ag-grid.md") ?? index2.getByPath("guides/migration-from-ag-grid");
575
+ const guideContent = entry ? entry.content : 'Migration guide not found. Please use `search_docs` with query "migration" to find available content.';
576
+ return {
577
+ messages: [
578
+ {
579
+ role: "user",
580
+ content: {
581
+ type: "text",
582
+ text: [
583
+ "I want to migrate my project from AG Grid to OGrid. Use the following migration guide to help me step by step.",
584
+ "",
585
+ "---",
586
+ "",
587
+ guideContent,
588
+ "",
589
+ "---",
590
+ "",
591
+ "Please analyze my current AG Grid usage and provide specific migration steps. For each AG Grid API, prop, or pattern I use, show me the OGrid equivalent with a code example."
592
+ ].join("\n")
593
+ }
594
+ }
595
+ ]
596
+ };
597
+ }
598
+ );
599
+ server2.resource(
600
+ "doc-page",
601
+ new ResourceTemplate("ogrid://docs/{path}", {
602
+ list: async () => ({
603
+ resources: index2.entries.map((entry) => ({
604
+ uri: `ogrid://docs/${toResourcePath(entry.path)}`,
605
+ name: entry.title,
606
+ description: entry.description,
607
+ mimeType: "text/markdown"
608
+ }))
609
+ })
610
+ }),
611
+ { description: 'OGrid documentation page. Use path like "features/filtering" or "api/column-def".', mimeType: "text/markdown" },
612
+ async (uri, { path }) => {
613
+ const entry = fromResourcePath(path, index2);
614
+ if (!entry) {
615
+ return {
616
+ contents: [
617
+ {
618
+ uri: uri.href,
619
+ mimeType: "text/markdown",
620
+ text: `# Not Found
621
+
622
+ No documentation found at path: "${path}"
623
+
624
+ Use \`list_docs\` tool or \`resources/list\` to see available paths.`
625
+ }
626
+ ]
627
+ };
628
+ }
629
+ return {
630
+ contents: [
631
+ {
632
+ uri: uri.href,
633
+ mimeType: "text/markdown",
634
+ text: [
635
+ `# ${entry.title}`,
636
+ `> ${entry.description}`,
637
+ "",
638
+ entry.content
639
+ ].join("\n")
640
+ }
641
+ ]
642
+ };
643
+ }
644
+ );
645
+ if (bridge) {
646
+ server2.tool(
647
+ "list_grids",
648
+ "List OGrid instances currently connected to the live testing bridge. Returns grid IDs, row counts, page info, and last-seen timestamps.",
649
+ {},
650
+ async () => {
651
+ const grids = bridge.listGrids();
652
+ if (grids.length === 0) {
653
+ return {
654
+ content: [
655
+ {
656
+ type: "text",
657
+ text: [
658
+ "No OGrid instances connected.",
659
+ "",
660
+ "To connect your app, add the bridge client:",
661
+ "```js",
662
+ "import { connectGridToBridge } from '@alaarab/ogrid-mcp/bridge-client';",
663
+ "const bridge = connectGridToBridge({ gridId: 'my-grid', getData: () => rows, getColumns: () => columns });",
664
+ "```",
665
+ "",
666
+ "Then start the MCP server with bridge enabled:",
667
+ " OGRID_BRIDGE_PORT=7890 npx @alaarab/ogrid-mcp",
668
+ " \u2014 or \u2014",
669
+ " npx @alaarab/ogrid-mcp --bridge"
670
+ ].join("\n")
671
+ }
672
+ ]
673
+ };
674
+ }
675
+ const formatted = grids.map((g) => {
676
+ const age = Math.round((Date.now() - g.lastSeen) / 1e3);
677
+ return [
678
+ `**${g.gridId}**`,
679
+ ` Rows: ${g.rowCount} displayed / ${g.totalCount} total`,
680
+ ` Page: ${g.page} / ${g.pageCount} (${g.pageSize} per page)`,
681
+ ` Columns: ${g.columns.map((c) => c.columnId).join(", ")}`,
682
+ ` Active filters: ${Object.keys(g.filterModel).length}`,
683
+ ` Last seen: ${age}s ago`
684
+ ].join("\n");
685
+ }).join("\n\n");
686
+ return {
687
+ content: [
688
+ {
689
+ type: "text",
690
+ text: `${grids.length} connected grid(s):
691
+
692
+ ${formatted}`
693
+ }
694
+ ]
695
+ };
696
+ }
697
+ );
698
+ server2.tool(
699
+ "get_grid_state",
700
+ "Get the current state of a connected OGrid instance: displayed rows, columns, sort, filters, pagination, and selection.",
701
+ {
702
+ gridId: z.string().describe("Grid ID as registered by connectGridToBridge()"),
703
+ includeData: z.boolean().optional().describe("Whether to include the full row data (default: false \u2014 shows only summary)"),
704
+ maxRows: z.number().int().min(1).max(200).optional().describe("Max rows to include when includeData=true (default: 20)")
705
+ },
706
+ async ({ gridId, includeData, maxRows }) => {
707
+ const state = bridge.getState(gridId);
708
+ if (!state) {
709
+ const available = bridge.listGrids().map((g) => g.gridId);
710
+ return {
711
+ content: [
712
+ {
713
+ type: "text",
714
+ text: available.length > 0 ? `Grid "${gridId}" not found. Available grids: ${available.join(", ")}` : `Grid "${gridId}" not found. No grids are currently connected.`
715
+ }
716
+ ]
717
+ };
718
+ }
719
+ const age = Math.round((Date.now() - state.lastSeen) / 1e3);
720
+ const colNames = state.columns.map((c) => `${c.columnId}${c.type ? ` (${c.type})` : ""}`).join(", ");
721
+ const sortDesc = state.sortModel.length > 0 ? state.sortModel.map((s) => `${s.columnId} ${s.direction}`).join(", ") : "none";
722
+ const filterDesc = Object.keys(state.filterModel).length > 0 ? JSON.stringify(state.filterModel) : "none";
723
+ const sections = [
724
+ `# Grid: ${gridId}`,
725
+ `Last seen: ${age}s ago`,
726
+ "",
727
+ `## Pagination`,
728
+ `Page ${state.page} of ${state.pageCount} | ${state.rowCount} rows displayed | ${state.totalCount} total`,
729
+ `Page size: ${state.pageSize}`,
730
+ "",
731
+ `## Columns (${state.columns.length})`,
732
+ colNames,
733
+ "",
734
+ `## Sort`,
735
+ sortDesc,
736
+ "",
737
+ `## Filters`,
738
+ filterDesc,
739
+ "",
740
+ `## Selection`,
741
+ state.selectedRowIndices.length > 0 ? `${state.selectedRowIndices.length} row(s) selected: indices [${state.selectedRowIndices.slice(0, 10).join(", ")}${state.selectedRowIndices.length > 10 ? ", ..." : ""}]` : "None"
742
+ ];
743
+ if (includeData) {
744
+ const limit = maxRows ?? 20;
745
+ const rows = state.data.slice(0, limit);
746
+ sections.push("", `## Data (first ${rows.length} of ${state.rowCount} rows)`);
747
+ sections.push("```json");
748
+ sections.push(JSON.stringify(rows, null, 2));
749
+ sections.push("```");
750
+ if (state.rowCount > limit) {
751
+ sections.push(`
752
+ _${state.rowCount - limit} more rows not shown. Increase maxRows to see more._`);
753
+ }
754
+ }
755
+ return {
756
+ content: [{ type: "text", text: sections.join("\n") }]
757
+ };
758
+ }
759
+ );
760
+ server2.tool(
761
+ "send_grid_command",
762
+ [
763
+ "Send a command to a connected OGrid instance and wait for the result.",
764
+ "",
765
+ "Command types:",
766
+ " update_cell \u2014 { rowIndex: number, columnId: string, value: unknown }",
767
+ " set_filter \u2014 { columnId: string, value: string | string[] }",
768
+ " clear_filters \u2014 {}",
769
+ ' set_sort \u2014 { sortModel: [{ columnId, direction: "asc"|"desc" }] }',
770
+ " go_to_page \u2014 { page: number }"
771
+ ].join("\n"),
772
+ {
773
+ gridId: z.string().describe("Grid ID as registered by connectGridToBridge()"),
774
+ type: z.enum(["update_cell", "set_filter", "clear_filters", "set_sort", "go_to_page"]).describe("Command type"),
775
+ payload: z.record(z.unknown()).describe("Command-specific payload (see tool description for fields per type)"),
776
+ timeoutMs: z.number().int().min(100).max(3e4).optional().describe("How long to wait for the app to execute the command (default: 5000ms)")
777
+ },
778
+ async ({ gridId, type, payload, timeoutMs }) => {
779
+ const state = bridge.getState(gridId);
780
+ if (!state) {
781
+ const available = bridge.listGrids().map((g) => g.gridId);
782
+ return {
783
+ content: [
784
+ {
785
+ type: "text",
786
+ text: available.length > 0 ? `Grid "${gridId}" not found. Available: ${available.join(", ")}` : `Grid "${gridId}" not found. No grids are currently connected.`
787
+ }
788
+ ]
789
+ };
790
+ }
791
+ const cmd = bridge.enqueueCommand(gridId, type, payload);
792
+ if (!cmd) {
793
+ return {
794
+ content: [{ type: "text", text: `Failed to enqueue command for grid "${gridId}".` }]
795
+ };
796
+ }
797
+ try {
798
+ const result = await bridge.waitForResult(cmd.id, timeoutMs ?? 5e3);
799
+ if (result.status === "error") {
800
+ return {
801
+ content: [
802
+ {
803
+ type: "text",
804
+ text: `Command failed: ${result.error ?? "unknown error"}
805
+
806
+ Command: ${JSON.stringify({ type, payload }, null, 2)}`
807
+ }
808
+ ]
809
+ };
810
+ }
811
+ return {
812
+ content: [
813
+ {
814
+ type: "text",
815
+ text: [
816
+ `\u2705 Command executed successfully`,
817
+ "",
818
+ `Type: ${type}`,
819
+ `Payload: ${JSON.stringify(payload)}`,
820
+ `Result: ${JSON.stringify(result.result)}`,
821
+ "",
822
+ `Call get_grid_state to see the updated grid state.`
823
+ ].join("\n")
824
+ }
825
+ ]
826
+ };
827
+ } catch {
828
+ return {
829
+ content: [
830
+ {
831
+ type: "text",
832
+ text: [
833
+ `\u23F1\uFE0F Command timed out after ${timeoutMs ?? 5e3}ms.`,
834
+ "",
835
+ "The OGrid app may not be polling for commands. Check that:",
836
+ "1. connectGridToBridge() is active in your app",
837
+ "2. The pollIntervalMs is not too large (default: 500ms)",
838
+ `3. The grid ID matches: "${gridId}"`
839
+ ].join("\n")
840
+ }
841
+ ]
842
+ };
843
+ }
844
+ }
845
+ );
846
+ }
847
+ return server2;
848
+ }
849
+ var BridgeStore = class {
850
+ constructor() {
851
+ this.grids = /* @__PURE__ */ new Map();
852
+ this.commandQueues = /* @__PURE__ */ new Map();
853
+ this.commandResults = /* @__PURE__ */ new Map();
854
+ this.cmdSeq = 0;
855
+ }
856
+ // ---- Called by HTTP handler ----
857
+ upsertGrid(gridId, partial) {
858
+ const existing = this.grids.get(gridId);
859
+ const now = Date.now();
860
+ this.grids.set(gridId, {
861
+ connectedAt: existing?.connectedAt ?? now,
862
+ lastSeen: now,
863
+ rowCount: 0,
864
+ totalCount: 0,
865
+ page: 1,
866
+ pageSize: 50,
867
+ pageCount: 1,
868
+ data: [],
869
+ columns: [],
870
+ sortModel: [],
871
+ filterModel: {},
872
+ selectedRowIndices: [],
873
+ ...existing,
874
+ ...partial,
875
+ // gridId must always be the canonical value — set last so partial can't override it
876
+ gridId
877
+ });
878
+ if (!this.commandQueues.has(gridId)) {
879
+ this.commandQueues.set(gridId, []);
880
+ }
881
+ }
882
+ popPendingCommands(gridId) {
883
+ const queue = this.commandQueues.get(gridId) ?? [];
884
+ const pending = queue.filter((c) => c.status === "pending");
885
+ const unsent = pending.filter((c) => !c.sent);
886
+ for (const cmd of unsent) {
887
+ cmd.sent = true;
888
+ }
889
+ return unsent;
890
+ }
891
+ resolveCommand(cmdId, result, error) {
892
+ for (const queue of this.commandQueues.values()) {
893
+ const cmd = queue.find((c) => c.id === cmdId);
894
+ if (cmd) {
895
+ cmd.status = error ? "error" : "completed";
896
+ cmd.result = result;
897
+ cmd.error = error;
898
+ this.commandResults.set(cmdId, { ...cmd });
899
+ return;
900
+ }
901
+ }
902
+ }
903
+ // ---- Called by MCP tools ----
904
+ listGrids() {
905
+ const cutoff = Date.now() - 3e4;
906
+ return [...this.grids.values()].filter((g) => g.lastSeen > cutoff);
907
+ }
908
+ getState(gridId) {
909
+ return this.grids.get(gridId);
910
+ }
911
+ enqueueCommand(gridId, type, payload) {
912
+ if (!this.grids.has(gridId)) return null;
913
+ const cmd = {
914
+ id: `cmd-${++this.cmdSeq}-${Date.now()}`,
915
+ type,
916
+ payload,
917
+ createdAt: Date.now(),
918
+ status: "pending"
919
+ };
920
+ const queue = this.commandQueues.get(gridId) ?? [];
921
+ queue.push(cmd);
922
+ this.commandQueues.set(gridId, queue);
923
+ return cmd;
924
+ }
925
+ waitForResult(cmdId, timeoutMs = 5e3) {
926
+ return new Promise((resolve, reject) => {
927
+ const deadline = Date.now() + timeoutMs;
928
+ const poll = () => {
929
+ const cmd = this.commandResults.get(cmdId);
930
+ if (cmd) {
931
+ resolve(cmd);
932
+ return;
933
+ }
934
+ if (Date.now() > deadline) {
935
+ reject(new Error(`Command ${cmdId} timed out after ${timeoutMs}ms`));
936
+ return;
937
+ }
938
+ setTimeout(poll, 100);
939
+ };
940
+ poll();
941
+ });
942
+ }
943
+ };
944
+ function readBody(req) {
945
+ return new Promise((resolve, reject) => {
946
+ const chunks = [];
947
+ req.on("data", (c) => chunks.push(c));
948
+ req.on("end", () => {
949
+ try {
950
+ resolve(JSON.parse(Buffer.concat(chunks).toString("utf-8") || "null"));
951
+ } catch {
952
+ resolve(null);
953
+ }
954
+ });
955
+ req.on("error", reject);
956
+ });
957
+ }
958
+ function send(res, status, body) {
959
+ const json = JSON.stringify(body);
960
+ res.writeHead(status, {
961
+ "Content-Type": "application/json",
962
+ "Content-Length": Buffer.byteLength(json),
963
+ "Access-Control-Allow-Origin": "*",
964
+ "Access-Control-Allow-Methods": "GET, POST, PUT, OPTIONS",
965
+ "Access-Control-Allow-Headers": "Content-Type"
966
+ });
967
+ res.end(json);
968
+ }
969
+ function startBridgeServer(store, port = 7890) {
970
+ return new Promise((resolve, reject) => {
971
+ const httpServer = createServer(async (req, res) => {
972
+ if (req.method === "OPTIONS") {
973
+ res.writeHead(204, {
974
+ "Access-Control-Allow-Origin": "*",
975
+ "Access-Control-Allow-Methods": "GET, POST, PUT, OPTIONS",
976
+ "Access-Control-Allow-Headers": "Content-Type"
977
+ });
978
+ res.end();
979
+ return;
980
+ }
981
+ const url = new URL(req.url ?? "/", `http://localhost:${port}`);
982
+ const parts = url.pathname.replace(/^\//, "").split("/");
983
+ try {
984
+ if (req.method === "GET" && parts[0] === "health") {
985
+ send(res, 200, { ok: true, grids: store.listGrids().length });
986
+ return;
987
+ }
988
+ if (req.method === "POST" && parts[0] === "grids" && parts[1] === "connect") {
989
+ const body = await readBody(req);
990
+ const gridId = String(body?.["gridId"] ?? "");
991
+ if (!gridId) {
992
+ send(res, 400, { error: "gridId required" });
993
+ return;
994
+ }
995
+ store.upsertGrid(gridId, body);
996
+ send(res, 200, { ok: true });
997
+ return;
998
+ }
999
+ if (req.method === "PUT" && parts[0] === "grids" && parts[2] === "state") {
1000
+ const gridId = parts[1];
1001
+ const body = await readBody(req);
1002
+ store.upsertGrid(gridId, body);
1003
+ send(res, 200, { ok: true });
1004
+ return;
1005
+ }
1006
+ if (req.method === "GET" && parts[0] === "grids" && parts[2] === "commands") {
1007
+ const gridId = parts[1];
1008
+ const state = store.getState(gridId);
1009
+ if (state) store.upsertGrid(gridId, {});
1010
+ const cmds = store.popPendingCommands(gridId);
1011
+ send(res, 200, cmds);
1012
+ return;
1013
+ }
1014
+ if (req.method === "POST" && parts[0] === "grids" && parts[2] === "commands" && parts[4] === "result") {
1015
+ const cmdId = parts[3];
1016
+ const body = await readBody(req);
1017
+ store.resolveCommand(cmdId, body?.["result"], body?.["error"]);
1018
+ send(res, 200, { ok: true });
1019
+ return;
1020
+ }
1021
+ send(res, 404, { error: "Not found" });
1022
+ } catch (err) {
1023
+ send(res, 500, { error: String(err) });
1024
+ }
1025
+ });
1026
+ httpServer.on("error", reject);
1027
+ httpServer.listen(port, "127.0.0.1", () => {
1028
+ console.error(`[ogrid-mcp] Bridge server listening on http://localhost:${port}`);
1029
+ resolve(
1030
+ () => new Promise((res) => {
1031
+ httpServer.close(() => res());
1032
+ })
1033
+ );
1034
+ });
1035
+ });
1036
+ }
1037
+ if (process.argv.includes("--version") || process.argv.includes("-v")) {
1038
+ const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "../../package.json");
1039
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
1040
+ process.stdout.write(`${pkg.version}
1041
+ `);
1042
+ process.exit(0);
1043
+ }
1044
+ var __dirname$1 = dirname(fileURLToPath(import.meta.url));
1045
+ var monorepoDocs = join(__dirname$1, "../../../docs/docs");
1046
+ var bundledDocs = join(__dirname$1, "../../bundled-docs");
1047
+ var docsDir = process.env["OGRID_DOCS_PATH"] ?? (existsSync(monorepoDocs) ? monorepoDocs : bundledDocs);
1048
+ var index = loadDocsIndex(docsDir);
1049
+ var bridgeStore = new BridgeStore();
1050
+ var bridgePort = process.env["OGRID_BRIDGE_PORT"] ? parseInt(process.env["OGRID_BRIDGE_PORT"], 10) : process.argv.includes("--bridge") ? 7890 : null;
1051
+ if (bridgePort !== null) {
1052
+ try {
1053
+ await startBridgeServer(bridgeStore, bridgePort);
1054
+ } catch (err) {
1055
+ console.error(`[ogrid-mcp] Failed to start bridge server on port ${bridgePort}: ${String(err)}`);
1056
+ }
1057
+ }
1058
+ var server = createOGridMcpServer(index, bridgeStore);
1059
+ var transport = new StdioServerTransport();
1060
+ await server.connect(transport);