Skip to content

Commit 94d7183

Browse files
authored
feat(web-mcp): add optional WebMCP resource/plugin, demo & tests (#2599)
* feat(web-mcp): add optional WebMCP resource/plugin, demo & tests
1 parent 573c2c1 commit 94d7183

37 files changed

Lines changed: 2270 additions & 5 deletions

File tree

.github/skills/SKILLS.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
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.
66

77
Available skills
8-
- `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`)
8+
- `create-slickgrid-package.SKILL.md` — guidance for creating a new package/external resource in the monorepo. (path: `.github/skills/create-slickgrid-package.SKILL.md`)
9+
- `skill-webmcp.SKILL.md` — pointers for WebMCP (AI Toolkit) docs, code, demos and tests. (path: `.github/skills/skill-webmcp.SKILL.md`)
10+
- `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`)
911

10-
Add new skills by adding `.SKILL.md` files to this folder and include a short entry here.
12+
To add a new skill, create a `*.SKILL.md` file in this folder and add a short entry here.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Skill: WebMCP / AI Toolkit
2+
3+
Purpose
4+
- Help AI agents discover WebMCP (Model Context Protocol) tools, docs and interfaces for the WebMCP integration in this repo.
5+
6+
Quick summary
7+
- 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.
8+
- Tools of interest: `read_slickgrid_data_<uid>`, `get_slickgrid_schema_<uid>`, `get_slickgrid_state_<uid>`, `apply_slickgrid_state_<uid>`.
9+
10+
Where to read (priority order)
11+
1. Concept & examples: `docs/ai/ai-toolkit.md`
12+
2. Package README: `packages/web-mcp/README.md`
13+
3. Implementation & types: `packages/web-mcp/src/web-mcp.service.ts`
14+
4. Core/common interfaces used: `packages/common` (search for `CurrentFilter`, `CurrentSorter`, `ExternalResource`, `FilterService`, `GridService`)
15+
5. Demo using WebMCP: `demos/vanilla/src/examples/example43.ts` and `demos/vanilla/src/examples/example43.html`
16+
6. Unit tests: `packages/web-mcp/src/__tests__/web-mcp.service.spec.ts`
17+
18+
Notes for agents
19+
- Prefer docs for conceptual questions and examples. Inspect `web-mcp.service.ts` for precise tool names, input shapes and runtime behavior.
20+
- `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.
21+
- WebMCP is opt-in — only demos that instantiate `WebMcpService` will register tools. Confirm the demo before attempting to call `navigator.modelContext` tools.
22+
23+
Common search hints
24+
- Keywords: `WebMCP`, `WebMcpService`, `modelContext`, `apply_slickgrid_state`, `get_slickgrid_schema`.
25+
- Files: `packages/web-mcp/src/**/*.ts`, `packages/common/src/**/*.ts`, `docs/ai/**/*.md`, `demos/**/src/examples/**`.
26+
27+
Example checklist for answering requests about WebMCP
28+
- Link to `docs/ai/ai-toolkit.md` for overview and prompting guidance.
29+
- Quote the exact tool name pattern and input schema from `web-mcp.service.ts` when suggesting client invocations.
30+
- If recommending local testing, point to `demos/vanilla/example43` and the Model Context Tool Inspector link: https://github.com/beaufortfrancois/model-context-tool-inspector

demos/aurelia/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"@slickgrid-universal/rxjs-observable": "workspace:*",
4444
"@slickgrid-universal/sql": "workspace:*",
4545
"@slickgrid-universal/text-export": "workspace:*",
46+
"@slickgrid-universal/web-mcp": "workspace:*",
4647
"aurelia": "^2.0.0-rc.1",
4748
"aurelia-slickgrid": "workspace:*",
4849
"bootstrap": "catalog:",
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<div class="container-fluid">
2+
<h2>
3+
Example 54: AI / Web MCP Toolkit
4+
<span class="float-end">
5+
<a
6+
style="font-size: 18px"
7+
target="_blank"
8+
href="https://github.com/ghiscoding/slickgrid-universal/blob/master/demos/aurelia/src/examples/slickgrid/example54.ts"
9+
>
10+
<span class="mdi mdi-link-variant"></span> code
11+
</a>
12+
</span>
13+
<button
14+
class="ms-2 btn btn-outline-secondary btn-sm btn-icon"
15+
type="button"
16+
data-test="toggle-subtitle"
17+
click.trigger="toggleSubTitle()"
18+
>
19+
<span class="mdi mdi-information-outline" title="Toggle example sub-title details"></span>
20+
</button>
21+
</h2>
22+
23+
<div class="subtitle">
24+
Demonstrates the optional <code>@slickgrid-universal/web-mcp</code> package (<code>WebMcpService</code>), which exposes the grid as
25+
<a href="https://modelcontextprotocol.io" target="_blank">Model Context Protocol (MCP)</a> tools so that AI assistants can read and
26+
manipulate the grid via natural language. The buttons below simulate what an LLM would call — in a real WebMCP-capable browser the same
27+
methods are called automatically by the AI assistant via <code>navigator.modelContext</code>. See
28+
<a href="https://ghiscoding.gitbook.io/slickgrid-universal/ai/ai-toolkit" target="_blank">AI Toolkit docs</a> for full details.
29+
</div>
30+
31+
<div class="row" style="margin-bottom: 4px">
32+
<div class="col-md-12">
33+
<button class="btn btn-outline-secondary btn-xs btn-icon" click.trigger="showSchema()">&lt;/&gt; "getStructuredSchema()"</button>
34+
<button class="btn btn-outline-secondary btn-xs btn-icon" click.trigger="showState()">
35+
<span class="mdi mdi-eye-outline mr-1"></span> "getGridState()"
36+
</button>
37+
<button class="btn btn-outline-secondary btn-xs btn-icon" click.trigger="applyAiState()">
38+
🤖 "applyGridState()" — simulated LLM response
39+
</button>
40+
<button class="btn btn-outline-secondary btn-xs btn-icon" click.trigger="resetGrid()">
41+
<span class="mdi mdi-refresh mr-1"></span> Reset
42+
</button>
43+
</div>
44+
</div>
45+
46+
<aurelia-slickgrid
47+
grid-id="grid53"
48+
columns.bind="columns"
49+
options.bind="gridOptions"
50+
dataset.bind="dataset"
51+
on-aurelia-grid-created.trigger="aureliaGridReady($event.detail)"
52+
>
53+
</aurelia-slickgrid>
54+
55+
<div class="row mt-2">
56+
<h6 class="label is-small">Output (what the LLM sees / sends)</h6>
57+
<pre
58+
id="mcp-output"
59+
textContent.bind="textResult"
60+
style="
61+
background-color: rgb(43, 43, 43);
62+
color: rgb(0, 187, 0);
63+
min-height: 260px;
64+
font-size: 0.75rem;
65+
overflow: auto;
66+
border-radius: 6px;
67+
white-space: pre-wrap;
68+
"
69+
></pre>
70+
</div>
71+
</div>
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { WebMcpService, type SlickGridState } from '@slickgrid-universal/web-mcp';
2+
import { bindable } from 'aurelia';
3+
import { type AureliaGridInstance, type Column, type GridOption } from 'aurelia-slickgrid';
4+
5+
const NB_ITEMS = 2000;
6+
const PRIORITIES = ['Low', 'Medium', 'High', 'Critical'];
7+
const STATUSES = ['Todo', 'In Progress', 'Done', 'Blocked'];
8+
9+
export class Example54 {
10+
@bindable() textResult = '';
11+
aureliaGrid!: AureliaGridInstance;
12+
columns: Column[] = [];
13+
gridContainerElm!: HTMLDivElement;
14+
gridOptions!: GridOption;
15+
dataset: any[] = [];
16+
hideSubTitle = false;
17+
mcpService = new WebMcpService();
18+
19+
constructor() {
20+
this.defineGrid();
21+
this.showOutput(
22+
'// 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.'
23+
);
24+
}
25+
26+
attached() {
27+
this.dataset = this.loadData(NB_ITEMS);
28+
}
29+
30+
aureliaGridReady(aureliaGrid: AureliaGridInstance) {
31+
this.aureliaGrid = aureliaGrid;
32+
}
33+
34+
defineGrid() {
35+
this.columns = [
36+
{ id: 'id', name: '#', field: 'id', sortable: true, width: 50 },
37+
{ id: 'title', name: 'Title', field: 'title', sortable: true, filterable: true, width: 200 },
38+
{ id: 'priority', name: 'Priority', field: 'priority', sortable: true, filterable: true, width: 110 },
39+
{ id: 'status', name: 'Status', field: 'status', sortable: true, filterable: true, width: 120 },
40+
{ id: 'duration', name: 'Duration (days)', field: 'duration', sortable: true, filterable: true, type: 'number', width: 140 },
41+
{ id: 'completed', name: 'Completed %', field: 'completed', sortable: true, filterable: true, type: 'number', width: 130 },
42+
];
43+
44+
this.gridOptions = {
45+
enableFiltering: true,
46+
enableSorting: true,
47+
gridHeight: 300,
48+
gridWidth: 800,
49+
externalResources: [this.mcpService],
50+
};
51+
}
52+
53+
// ---------------------------------------------------------------------------
54+
// Button handlers — simulating what an LLM would call via WebMCP tools
55+
// ---------------------------------------------------------------------------
56+
57+
showSchema() {
58+
const schema = this.mcpService.getStructuredSchema();
59+
this.showOutput(JSON.stringify(schema, null, 2));
60+
}
61+
62+
showState() {
63+
const state = this.mcpService.getGridState();
64+
this.showOutput(JSON.stringify(state, null, 2));
65+
}
66+
67+
/** Simulate a typical LLM response: filter to High/Critical priority, sort by duration desc */
68+
async applyAiState() {
69+
const aiGeneratedState: Partial<SlickGridState> = {
70+
filters: [{ columnId: 'priority', searchTerms: ['High'], operator: 'EQ' }],
71+
sorters: [{ columnId: 'duration', direction: 'DESC' }],
72+
};
73+
this.showOutput(`// Simulated LLM response — applying state:\n${JSON.stringify(aiGeneratedState, null, 2)}`);
74+
await this.mcpService.applyGridState(aiGeneratedState);
75+
}
76+
77+
async resetGrid() {
78+
await this.mcpService.applyGridState({ filters: [], sorters: [] });
79+
this.showOutput('// Grid state reset.');
80+
}
81+
82+
toggleSubTitle() {
83+
this.hideSubTitle = !this.hideSubTitle;
84+
const action = this.hideSubTitle ? 'add' : 'remove';
85+
document.querySelector('.subtitle')?.classList[action]('hidden');
86+
this.aureliaGrid.resizerService.resizeGrid(0);
87+
}
88+
89+
// ---------------------------------------------------------------------------
90+
// Helpers
91+
// ---------------------------------------------------------------------------
92+
93+
private showOutput(text: string) {
94+
this.textResult = text;
95+
}
96+
97+
private loadData(count: number): any[] {
98+
const data: any[] = [];
99+
for (let i = 0; i < count; i++) {
100+
data.push({
101+
id: i,
102+
title: `Task ${i}`,
103+
priority: PRIORITIES[Math.floor(Math.random() * PRIORITIES.length)],
104+
status: STATUSES[Math.floor(Math.random() * STATUSES.length)],
105+
duration: Math.floor(Math.random() * 90) + 1,
106+
completed: Math.floor(Math.random() * 100),
107+
});
108+
}
109+
return data;
110+
}
111+
}

demos/aurelia/src/my-app.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ const myRoutes: Routeable[] = [
5959
{ path: 'example51', component: () => import('./examples/slickgrid/example51.js'), title: '51- Menus with Slots' },
6060
{ path: 'example52', component: () => import('./examples/slickgrid/example52.js'), title: '52- SQL Backend Service' },
6161
{ path: 'example53', component: () => import('./examples/slickgrid/example53.js'), title: '53- Custom Filter Bar' },
62+
{ path: 'example54', component: () => import('./examples/slickgrid/example54.js'), title: '54- AI / Web MCP Toolkit' },
6263
{ path: 'home', component: () => import('./home-page.js'), title: 'Home' },
6364
];
6465
@route({
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
describe('Example 54 - AI / Web MCP Toolkit', () => {
2+
const titles = ['#', 'Title', 'Priority', 'Status', 'Duration (days)', 'Completed %'];
3+
4+
it('should display Example title', () => {
5+
cy.visit(`${Cypress.config('baseUrl')}/example54`);
6+
cy.get('h2').should('contain', 'Example 54: AI / Web MCP Toolkit');
7+
});
8+
9+
it('should have exact Column Titles in the grid', () => {
10+
cy.get('.slick-header-columns .slick-column-name').each(($child, index) => expect($child.text()).to.eq(titles[index]));
11+
});
12+
13+
it('getStructuredSchema() should return column metadata containing "priority"', () => {
14+
cy.contains('getStructuredSchema()').click();
15+
cy.get('#mcp-output').should('contain', 'priority');
16+
});
17+
18+
it('applyGridState() should apply simulated LLM state (filter priority=High + sort duration desc)', () => {
19+
// Apply simulated AI state
20+
cy.contains('applyGridState()').click();
21+
22+
// Wait a short time for state to be applied then inspect grid state
23+
cy.contains('getGridState()').click();
24+
25+
cy.get('#mcp-output')
26+
.invoke('text')
27+
.then((text) => {
28+
const state = JSON.parse(text);
29+
expect(state).to.have.property('filters');
30+
expect(state.filters).to.be.an('array').and.to.have.length.greaterThan(0);
31+
expect(state.filters[0].columnId).to.equal('priority');
32+
});
33+
34+
// Verify first row priority cell contains 'High'
35+
cy.get('[data-row="0"] > .slick-cell:nth(2)').should('contain', 'High');
36+
});
37+
38+
it('reset should clear filters (getGridState shows empty filters)', () => {
39+
cy.contains('Reset').click();
40+
cy.contains('getGridState()').click();
41+
cy.get('#mcp-output')
42+
.invoke('text')
43+
.then((text) => {
44+
const state = JSON.parse(text);
45+
expect(state.filters).to.be.an('array').and.to.have.length(0);
46+
});
47+
});
48+
});

demos/react/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"@slickgrid-universal/rxjs-observable": "workspace:*",
3737
"@slickgrid-universal/sql": "workspace:*",
3838
"@slickgrid-universal/text-export": "workspace:*",
39+
"@slickgrid-universal/web-mcp": "workspace:*",
3940
"bootstrap": "catalog:",
4041
"dompurify": "catalog:",
4142
"i18next": "catalog:",

demos/react/src/examples/slickgrid/App.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ const routes = [
5555
{ path: 'example51', route: '/example51', element: lazy(() => import('./Example51.js')), title: '51- Menus with Slots' },
5656
{ path: 'example52', route: '/example52', element: lazy(() => import('./Example52.js')), title: '52- SQL Backend Service' },
5757
{ path: 'example53', route: '/example53', element: lazy(() => import('./Example53.js')), title: '53- Custom Filter Bar' },
58+
{ path: 'example54', route: '/example54', element: lazy(() => import('./Example54.js')), title: '54- AI / Web MCP Toolkit' },
5859
];
5960

6061
export default function Routes() {

0 commit comments

Comments
 (0)