Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .github/skills/SKILLS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
This folder contains repo-level AI skill files. Each skill is a `.SKILL.md` file describing how an AI assistant should behave or where to look for information.

Available skills
- `skill-read-docs-and-interfaces.SKILL.md` — Guides agents where to find docs and TypeScript interfaces in the monorepo. (path: `.github/skills/skill-read-docs-and-interfaces.SKILL.md`)
- `create-slickgrid-package.SKILL.md` — guidance for creating a new package/external resource in the monorepo. (path: `.github/skills/create-slickgrid-package.SKILL.md`)
- `skill-webmcp.SKILL.md` — pointers for WebMCP (AI Toolkit) docs, code, demos and tests. (path: `.github/skills/skill-webmcp.SKILL.md`)
- `skill-read-docs-and-interfaces.SKILL.md` — guides agents where to find docs and TypeScript interfaces in the monorepo. (path: `.github/skills/skill-read-docs-and-interfaces.SKILL.md`)

Add new skills by adding `.SKILL.md` files to this folder and include a short entry here.
To add a new skill, create a `*.SKILL.md` file in this folder and add a short entry here.
30 changes: 30 additions & 0 deletions .github/skills/skill-webmcp.SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Skill: WebMCP / AI Toolkit

Purpose
- Help AI agents discover WebMCP (Model Context Protocol) tools, docs and interfaces for the WebMCP integration in this repo.

Quick summary
- The WebMCP feature is implemented in the `packages/web-mcp` package. It registers MCP tools when `navigator.modelContext` is available and exposes helpers usable directly via the `WebMcpService` instance.
- Tools of interest: `read_slickgrid_data_<uid>`, `get_slickgrid_schema_<uid>`, `get_slickgrid_state_<uid>`, `apply_slickgrid_state_<uid>`.

Where to read (priority order)
1. Concept & examples: `docs/ai/ai-toolkit.md`
2. Package README: `packages/web-mcp/README.md`
3. Implementation & types: `packages/web-mcp/src/web-mcp.service.ts`
4. Core/common interfaces used: `packages/common` (search for `CurrentFilter`, `CurrentSorter`, `ExternalResource`, `FilterService`, `GridService`)
5. Demo using WebMCP: `demos/vanilla/src/examples/example43.ts` and `demos/vanilla/src/examples/example43.html`
6. Unit tests: `packages/web-mcp/src/__tests__/web-mcp.service.spec.ts`

Notes for agents
- Prefer docs for conceptual questions and examples. Inspect `web-mcp.service.ts` for precise tool names, input shapes and runtime behavior.
- `apply_slickgrid_state` performs basic validation; the service returns structured error objects for malformed input instead of throwing at the tool boundary. Use the tests as concrete examples of validation behavior.
- WebMCP is opt-in — only demos that instantiate `WebMcpService` will register tools. Confirm the demo before attempting to call `navigator.modelContext` tools.

Common search hints
- Keywords: `WebMCP`, `WebMcpService`, `modelContext`, `apply_slickgrid_state`, `get_slickgrid_schema`.
- Files: `packages/web-mcp/src/**/*.ts`, `packages/common/src/**/*.ts`, `docs/ai/**/*.md`, `demos/**/src/examples/**`.

Example checklist for answering requests about WebMCP
- Link to `docs/ai/ai-toolkit.md` for overview and prompting guidance.
- Quote the exact tool name pattern and input schema from `web-mcp.service.ts` when suggesting client invocations.
- If recommending local testing, point to `demos/vanilla/example43` and the Model Context Tool Inspector link: https://github.com/beaufortfrancois/model-context-tool-inspector
1 change: 1 addition & 0 deletions demos/aurelia/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"@slickgrid-universal/rxjs-observable": "workspace:*",
"@slickgrid-universal/sql": "workspace:*",
"@slickgrid-universal/text-export": "workspace:*",
"@slickgrid-universal/web-mcp": "workspace:*",
"aurelia": "^2.0.0-rc.1",
"aurelia-slickgrid": "workspace:*",
"bootstrap": "catalog:",
Expand Down
71 changes: 71 additions & 0 deletions demos/aurelia/src/examples/slickgrid/example54.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<div class="container-fluid">
<h2>
Example 54: AI / Web MCP Toolkit
<span class="float-end">
<a
style="font-size: 18px"
target="_blank"
href="https://github.com/ghiscoding/slickgrid-universal/blob/master/demos/aurelia/src/examples/slickgrid/example54.ts"
>
<span class="mdi mdi-link-variant"></span> code
</a>
</span>
<button
class="ms-2 btn btn-outline-secondary btn-sm btn-icon"
type="button"
data-test="toggle-subtitle"
click.trigger="toggleSubTitle()"
>
<span class="mdi mdi-information-outline" title="Toggle example sub-title details"></span>
</button>
</h2>

<div class="subtitle">
Demonstrates the optional <code>@slickgrid-universal/web-mcp</code> package (<code>WebMcpService</code>), which exposes the grid as
<a href="https://modelcontextprotocol.io" target="_blank">Model Context Protocol (MCP)</a> tools so that AI assistants can read and
manipulate the grid via natural language. The buttons below simulate what an LLM would call — in a real WebMCP-capable browser the same
methods are called automatically by the AI assistant via <code>navigator.modelContext</code>. See
<a href="https://ghiscoding.gitbook.io/slickgrid-universal/ai/ai-toolkit" target="_blank">AI Toolkit docs</a> for full details.
</div>

<div class="row" style="margin-bottom: 4px">
<div class="col-md-12">
<button class="btn btn-outline-secondary btn-xs btn-icon" click.trigger="showSchema()">&lt;/&gt; "getStructuredSchema()"</button>
<button class="btn btn-outline-secondary btn-xs btn-icon" click.trigger="showState()">
<span class="mdi mdi-eye-outline mr-1"></span> "getGridState()"
</button>
<button class="btn btn-outline-secondary btn-xs btn-icon" click.trigger="applyAiState()">
🤖 "applyGridState()" — simulated LLM response
</button>
<button class="btn btn-outline-secondary btn-xs btn-icon" click.trigger="resetGrid()">
<span class="mdi mdi-refresh mr-1"></span> Reset
</button>
</div>
</div>

<aurelia-slickgrid
grid-id="grid53"
columns.bind="columns"
options.bind="gridOptions"
dataset.bind="dataset"
on-aurelia-grid-created.trigger="aureliaGridReady($event.detail)"
>
</aurelia-slickgrid>

<div class="row mt-2">
<h6 class="label is-small">Output (what the LLM sees / sends)</h6>
<pre
id="mcp-output"
textContent.bind="textResult"
style="
background-color: rgb(43, 43, 43);
color: rgb(0, 187, 0);
min-height: 260px;
font-size: 0.75rem;
overflow: auto;
border-radius: 6px;
white-space: pre-wrap;
"
></pre>
</div>
</div>
111 changes: 111 additions & 0 deletions demos/aurelia/src/examples/slickgrid/example54.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { WebMcpService, type SlickGridState } from '@slickgrid-universal/web-mcp';
import { bindable } from 'aurelia';
import { type AureliaGridInstance, type Column, type GridOption } from 'aurelia-slickgrid';

const NB_ITEMS = 2000;
const PRIORITIES = ['Low', 'Medium', 'High', 'Critical'];
const STATUSES = ['Todo', 'In Progress', 'Done', 'Blocked'];

export class Example54 {
@bindable() textResult = '';
aureliaGrid!: AureliaGridInstance;
columns: Column[] = [];
gridContainerElm!: HTMLDivElement;
gridOptions!: GridOption;
dataset: any[] = [];
hideSubTitle = false;
mcpService = new WebMcpService();

constructor() {
this.defineGrid();
this.showOutput(
'// Click a button above to inspect or manipulate the grid via the MCP service API.\n// In a real WebMCP-capable browser, an AI assistant calls these same methods automatically.'
);
}

attached() {
this.dataset = this.loadData(NB_ITEMS);
}

aureliaGridReady(aureliaGrid: AureliaGridInstance) {
this.aureliaGrid = aureliaGrid;
}

defineGrid() {
this.columns = [
{ id: 'id', name: '#', field: 'id', sortable: true, width: 50 },
{ id: 'title', name: 'Title', field: 'title', sortable: true, filterable: true, width: 200 },
{ id: 'priority', name: 'Priority', field: 'priority', sortable: true, filterable: true, width: 110 },
{ id: 'status', name: 'Status', field: 'status', sortable: true, filterable: true, width: 120 },
{ id: 'duration', name: 'Duration (days)', field: 'duration', sortable: true, filterable: true, type: 'number', width: 140 },
{ id: 'completed', name: 'Completed %', field: 'completed', sortable: true, filterable: true, type: 'number', width: 130 },
];

this.gridOptions = {
enableFiltering: true,
enableSorting: true,
gridHeight: 300,
gridWidth: 800,
externalResources: [this.mcpService],
};
}

// ---------------------------------------------------------------------------
// Button handlers — simulating what an LLM would call via WebMCP tools
// ---------------------------------------------------------------------------

showSchema() {
const schema = this.mcpService.getStructuredSchema();
this.showOutput(JSON.stringify(schema, null, 2));
}

showState() {
const state = this.mcpService.getGridState();
this.showOutput(JSON.stringify(state, null, 2));
}

/** Simulate a typical LLM response: filter to High/Critical priority, sort by duration desc */
async applyAiState() {
const aiGeneratedState: Partial<SlickGridState> = {
filters: [{ columnId: 'priority', searchTerms: ['High'], operator: 'EQ' }],
sorters: [{ columnId: 'duration', direction: 'DESC' }],
};
this.showOutput(`// Simulated LLM response — applying state:\n${JSON.stringify(aiGeneratedState, null, 2)}`);
await this.mcpService.applyGridState(aiGeneratedState);
}

async resetGrid() {
await this.mcpService.applyGridState({ filters: [], sorters: [] });
this.showOutput('// Grid state reset.');
}

toggleSubTitle() {
this.hideSubTitle = !this.hideSubTitle;
const action = this.hideSubTitle ? 'add' : 'remove';
document.querySelector('.subtitle')?.classList[action]('hidden');
this.aureliaGrid.resizerService.resizeGrid(0);
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

private showOutput(text: string) {
this.textResult = text;
}

private loadData(count: number): any[] {
const data: any[] = [];
for (let i = 0; i < count; i++) {
data.push({
id: i,
title: `Task ${i}`,
priority: PRIORITIES[Math.floor(Math.random() * PRIORITIES.length)],
status: STATUSES[Math.floor(Math.random() * STATUSES.length)],
duration: Math.floor(Math.random() * 90) + 1,
completed: Math.floor(Math.random() * 100),
});
}
return data;
}
}
1 change: 1 addition & 0 deletions demos/aurelia/src/my-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const myRoutes: Routeable[] = [
{ path: 'example51', component: () => import('./examples/slickgrid/example51.js'), title: '51- Menus with Slots' },
{ path: 'example52', component: () => import('./examples/slickgrid/example52.js'), title: '52- SQL Backend Service' },
{ path: 'example53', component: () => import('./examples/slickgrid/example53.js'), title: '53- Custom Filter Bar' },
{ path: 'example54', component: () => import('./examples/slickgrid/example54.js'), title: '54- AI / Web MCP Toolkit' },
{ path: 'home', component: () => import('./home-page.js'), title: 'Home' },
];
@route({
Expand Down
48 changes: 48 additions & 0 deletions demos/aurelia/test/cypress/e2e/example54.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
describe('Example 54 - AI / Web MCP Toolkit', () => {
const titles = ['#', 'Title', 'Priority', 'Status', 'Duration (days)', 'Completed %'];

it('should display Example title', () => {
cy.visit(`${Cypress.config('baseUrl')}/example54`);
cy.get('h2').should('contain', 'Example 54: AI / Web MCP Toolkit');
});

it('should have exact Column Titles in the grid', () => {
cy.get('.slick-header-columns .slick-column-name').each(($child, index) => expect($child.text()).to.eq(titles[index]));
});

it('getStructuredSchema() should return column metadata containing "priority"', () => {
cy.contains('getStructuredSchema()').click();
cy.get('#mcp-output').should('contain', 'priority');
});

it('applyGridState() should apply simulated LLM state (filter priority=High + sort duration desc)', () => {
// Apply simulated AI state
cy.contains('applyGridState()').click();

// Wait a short time for state to be applied then inspect grid state
cy.contains('getGridState()').click();

cy.get('#mcp-output')
.invoke('text')
.then((text) => {
const state = JSON.parse(text);
expect(state).to.have.property('filters');
expect(state.filters).to.be.an('array').and.to.have.length.greaterThan(0);
expect(state.filters[0].columnId).to.equal('priority');
});

// Verify first row priority cell contains 'High'
cy.get('[data-row="0"] > .slick-cell:nth(2)').should('contain', 'High');
});

it('reset should clear filters (getGridState shows empty filters)', () => {
cy.contains('Reset').click();
cy.contains('getGridState()').click();
cy.get('#mcp-output')
.invoke('text')
.then((text) => {
const state = JSON.parse(text);
expect(state.filters).to.be.an('array').and.to.have.length(0);
});
});
});
1 change: 1 addition & 0 deletions demos/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"@slickgrid-universal/rxjs-observable": "workspace:*",
"@slickgrid-universal/sql": "workspace:*",
"@slickgrid-universal/text-export": "workspace:*",
"@slickgrid-universal/web-mcp": "workspace:*",
"bootstrap": "catalog:",
"dompurify": "catalog:",
"i18next": "catalog:",
Expand Down
1 change: 1 addition & 0 deletions demos/react/src/examples/slickgrid/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ const routes = [
{ path: 'example51', route: '/example51', element: lazy(() => import('./Example51.js')), title: '51- Menus with Slots' },
{ path: 'example52', route: '/example52', element: lazy(() => import('./Example52.js')), title: '52- SQL Backend Service' },
{ path: 'example53', route: '/example53', element: lazy(() => import('./Example53.js')), title: '53- Custom Filter Bar' },
{ path: 'example54', route: '/example54', element: lazy(() => import('./Example54.js')), title: '54- AI / Web MCP Toolkit' },
];

export default function Routes() {
Expand Down
Loading
Loading