@svgrid/mcp 2.6.8 → 3.0.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.
- package/README.md +37 -22
- package/dist/core-tools.d.ts +74 -0
- package/dist/core-tools.js +362 -0
- package/dist/data.js +5 -5
- package/dist/index.d.ts +11 -5
- package/dist/index.js +244 -354
- package/dist/installed.d.ts +33 -0
- package/dist/installed.js +87 -0
- package/dist/preview.d.ts +88 -0
- package/dist/preview.js +211 -0
- package/dist/prompts.d.ts +31 -0
- package/dist/prompts.js +65 -0
- package/dist/resources.d.ts +19 -0
- package/dist/resources.js +84 -0
- package/dist/search.d.ts +11 -0
- package/dist/search.js +28 -0
- package/dist/studio-tools.d.ts +13 -0
- package/dist/studio-tools.js +227 -0
- package/dist/validate.d.ts +40 -0
- package/dist/validate.js +70 -5
- package/package.json +1 -1
- package/server.json +2 -2
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type InstalledGrid = {
|
|
2
|
+
version: string;
|
|
3
|
+
/** Where it was found, so a surprising answer can be traced. */
|
|
4
|
+
path: string;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Walk up from `from` looking for `node_modules/@svgrid/grid/package.json`.
|
|
8
|
+
*
|
|
9
|
+
* Nearest-first, and it stops at the first hit: a monorepo can legitimately
|
|
10
|
+
* hold several copies, and guessing which one the user means would produce
|
|
11
|
+
* confident nonsense. The nearest one to the working directory is the one a
|
|
12
|
+
* build would resolve.
|
|
13
|
+
*/
|
|
14
|
+
export declare function findInstalledGrid(from?: string): InstalledGrid | null;
|
|
15
|
+
/** Cached lookup. The filesystem does not change under a running server. */
|
|
16
|
+
export declare function installedGrid(): InstalledGrid | null;
|
|
17
|
+
/** Test seam: forget what was found, so a test can point at another tree. */
|
|
18
|
+
export declare function resetInstalledGrid(next?: InstalledGrid | null): void;
|
|
19
|
+
export type VersionNote = {
|
|
20
|
+
/** The version this server's corpus and API surface describe. */
|
|
21
|
+
corpus: string;
|
|
22
|
+
/** What is actually installed where the server is running, if anything. */
|
|
23
|
+
installed?: string;
|
|
24
|
+
/** Present only when they disagree - the part a model must not ignore. */
|
|
25
|
+
warning?: string;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* How to describe the corpus/installed pairing in a tool result.
|
|
29
|
+
*
|
|
30
|
+
* Silent when they match or when nothing is installed. A note that fires
|
|
31
|
+
* constantly is a note that gets skipped.
|
|
32
|
+
*/
|
|
33
|
+
export declare function versionNote(corpusVersion: string): VersionNote | undefined;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which version of SvGrid the person asking actually has installed.
|
|
3
|
+
*
|
|
4
|
+
* This is the thing a proxy-shaped MCP server cannot do. Telerik and Syncfusion
|
|
5
|
+
* answer every question from a backend that serves one global "latest" - their
|
|
6
|
+
* server has no idea what is in your `node_modules`, so a model can be told
|
|
7
|
+
* about an API you do not have, confidently and with a citation.
|
|
8
|
+
*
|
|
9
|
+
* We ship the corpus, so we can do better: read the consumer's installed grid
|
|
10
|
+
* and say plainly when it disagrees with what this corpus describes. A warning
|
|
11
|
+
* that the answer may not apply is worth far more than a confident wrong one.
|
|
12
|
+
*
|
|
13
|
+
* Absent is the normal case, not an error - the server is often run from a
|
|
14
|
+
* directory that has no SvGrid in it at all (a fresh project, a chat with no
|
|
15
|
+
* workspace). Say nothing then rather than inventing a mismatch.
|
|
16
|
+
*/
|
|
17
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
18
|
+
import { dirname, join, parse } from 'node:path';
|
|
19
|
+
let cached;
|
|
20
|
+
/**
|
|
21
|
+
* Walk up from `from` looking for `node_modules/@svgrid/grid/package.json`.
|
|
22
|
+
*
|
|
23
|
+
* Nearest-first, and it stops at the first hit: a monorepo can legitimately
|
|
24
|
+
* hold several copies, and guessing which one the user means would produce
|
|
25
|
+
* confident nonsense. The nearest one to the working directory is the one a
|
|
26
|
+
* build would resolve.
|
|
27
|
+
*/
|
|
28
|
+
export function findInstalledGrid(from = process.cwd()) {
|
|
29
|
+
let dir = from;
|
|
30
|
+
const { root } = parse(dir);
|
|
31
|
+
for (;;) {
|
|
32
|
+
const manifest = join(dir, 'node_modules', '@svgrid', 'grid', 'package.json');
|
|
33
|
+
if (existsSync(manifest)) {
|
|
34
|
+
try {
|
|
35
|
+
const version = JSON.parse(readFileSync(manifest, 'utf8')).version;
|
|
36
|
+
if (typeof version === 'string' && version)
|
|
37
|
+
return { version, path: manifest };
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// An unreadable manifest is the same as not finding one: this is a
|
|
41
|
+
// courtesy check, and it must never be the reason a tool call fails.
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
if (dir === root)
|
|
46
|
+
return null;
|
|
47
|
+
const parent = dirname(dir);
|
|
48
|
+
if (parent === dir)
|
|
49
|
+
return null;
|
|
50
|
+
dir = parent;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** Cached lookup. The filesystem does not change under a running server. */
|
|
54
|
+
export function installedGrid() {
|
|
55
|
+
if (cached === undefined)
|
|
56
|
+
cached = findInstalledGrid();
|
|
57
|
+
return cached;
|
|
58
|
+
}
|
|
59
|
+
/** Test seam: forget what was found, so a test can point at another tree. */
|
|
60
|
+
export function resetInstalledGrid(next) {
|
|
61
|
+
cached = next;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* How to describe the corpus/installed pairing in a tool result.
|
|
65
|
+
*
|
|
66
|
+
* Silent when they match or when nothing is installed. A note that fires
|
|
67
|
+
* constantly is a note that gets skipped.
|
|
68
|
+
*/
|
|
69
|
+
export function versionNote(corpusVersion) {
|
|
70
|
+
const found = installedGrid();
|
|
71
|
+
// Nothing installed means nothing to add: the check result already carries
|
|
72
|
+
// `checkedAgainst` and says the version in its summary, so a bare
|
|
73
|
+
// `{ corpus }` would be the same fact a third time. A field that is usually
|
|
74
|
+
// redundant is a field that gets skipped when it finally matters.
|
|
75
|
+
if (!found)
|
|
76
|
+
return undefined;
|
|
77
|
+
if (found.version === corpusVersion) {
|
|
78
|
+
return { corpus: corpusVersion, installed: found.version };
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
corpus: corpusVersion,
|
|
82
|
+
installed: found.version,
|
|
83
|
+
warning: `This server describes @svgrid/grid@${corpusVersion}, but ${found.version} is installed ` +
|
|
84
|
+
`here. Treat anything version-specific as unverified for your version, and say so rather ` +
|
|
85
|
+
`than asserting it.`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/** MCP Apps constants, inlined rather than depending on the extension SDK server-side. */
|
|
2
|
+
export declare const UI_MIME_TYPE = "text/html;profile=mcp-app";
|
|
3
|
+
export declare const UI_RESOURCE_URI = "ui://svgrid/preview.html";
|
|
4
|
+
/**
|
|
5
|
+
* The published versions the preview loads from the CDN.
|
|
6
|
+
*
|
|
7
|
+
* Pinned, not `@latest`: a preview that silently follows a future major would
|
|
8
|
+
* start failing in a way nobody is watching for. `tools/mcp-tools.test.ts` checks these
|
|
9
|
+
* against the workspace so they cannot quietly rot either.
|
|
10
|
+
*/
|
|
11
|
+
export declare const PREVIEW_GRID_WC_VERSION = "2.7.0";
|
|
12
|
+
export declare function previewResource(): {
|
|
13
|
+
uri: string;
|
|
14
|
+
name: string;
|
|
15
|
+
title: string;
|
|
16
|
+
description: string;
|
|
17
|
+
mimeType: string;
|
|
18
|
+
_meta: {
|
|
19
|
+
'ui/csp': {
|
|
20
|
+
resourceDomains: string[];
|
|
21
|
+
};
|
|
22
|
+
'ui/prefersBorder': boolean;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
export declare function readPreviewResource(uri: string): {
|
|
26
|
+
contents: {
|
|
27
|
+
uri: string;
|
|
28
|
+
mimeType: string;
|
|
29
|
+
text: string;
|
|
30
|
+
}[];
|
|
31
|
+
} | undefined;
|
|
32
|
+
export declare const PREVIEW_TOOL: {
|
|
33
|
+
name: string;
|
|
34
|
+
title: string;
|
|
35
|
+
description: string;
|
|
36
|
+
inputSchema: {
|
|
37
|
+
type: string;
|
|
38
|
+
properties: {
|
|
39
|
+
columns: {
|
|
40
|
+
type: string;
|
|
41
|
+
description: string;
|
|
42
|
+
};
|
|
43
|
+
data: {
|
|
44
|
+
type: string;
|
|
45
|
+
description: string;
|
|
46
|
+
};
|
|
47
|
+
demo: {
|
|
48
|
+
type: string;
|
|
49
|
+
description: string;
|
|
50
|
+
};
|
|
51
|
+
title: {
|
|
52
|
+
type: string;
|
|
53
|
+
description: string;
|
|
54
|
+
};
|
|
55
|
+
sortable: {
|
|
56
|
+
type: string;
|
|
57
|
+
description: string;
|
|
58
|
+
};
|
|
59
|
+
filterable: {
|
|
60
|
+
type: string;
|
|
61
|
+
description: string;
|
|
62
|
+
};
|
|
63
|
+
pageable: {
|
|
64
|
+
type: string;
|
|
65
|
+
description: string;
|
|
66
|
+
};
|
|
67
|
+
groupBy: {
|
|
68
|
+
type: string;
|
|
69
|
+
description: string;
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
required: never[];
|
|
73
|
+
};
|
|
74
|
+
_meta: {
|
|
75
|
+
"ui/resourceUri": string;
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
type ToolResult = {
|
|
79
|
+
isError?: boolean;
|
|
80
|
+
content: {
|
|
81
|
+
type: 'text';
|
|
82
|
+
text: string;
|
|
83
|
+
}[];
|
|
84
|
+
structuredContent?: Record<string, unknown>;
|
|
85
|
+
_meta?: Record<string, unknown>;
|
|
86
|
+
};
|
|
87
|
+
export declare function handlePreview(args: Record<string, unknown>): ToolResult;
|
|
88
|
+
export {};
|
package/dist/preview.js
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A live, interactive grid rendered inside the conversation.
|
|
3
|
+
*
|
|
4
|
+
* Every other tool here hands the model TEXT and asks a person to imagine the
|
|
5
|
+
* result. This renders the real grid - sortable, filterable, scrollable - in
|
|
6
|
+
* the client, using MCP Apps, the official UI extension: a tool points at a
|
|
7
|
+
* `ui://` resource through `_meta`, the client loads that HTML in a sandboxed
|
|
8
|
+
* iframe, and the tool's `structuredContent` arrives over a postMessage bridge.
|
|
9
|
+
*
|
|
10
|
+
* It is the same custom element a real page would use, from the published
|
|
11
|
+
* package on a CDN - not a screenshot, not a mock table. What you see is what
|
|
12
|
+
* `<sv-grid>` does with those columns.
|
|
13
|
+
*
|
|
14
|
+
* Progressive enhancement is the rule the extension is built around: clients
|
|
15
|
+
* without MCP Apps ignore the `_meta` and still get the text summary, which is
|
|
16
|
+
* why the text half has to stand on its own rather than say "see the preview".
|
|
17
|
+
*/
|
|
18
|
+
import { examples } from './data.js';
|
|
19
|
+
/** MCP Apps constants, inlined rather than depending on the extension SDK server-side. */
|
|
20
|
+
export const UI_MIME_TYPE = 'text/html;profile=mcp-app';
|
|
21
|
+
export const UI_RESOURCE_URI = 'ui://svgrid/preview.html';
|
|
22
|
+
const UI_RESOURCE_META_KEY = 'ui/resourceUri';
|
|
23
|
+
/**
|
|
24
|
+
* The published versions the preview loads from the CDN.
|
|
25
|
+
*
|
|
26
|
+
* Pinned, not `@latest`: a preview that silently follows a future major would
|
|
27
|
+
* start failing in a way nobody is watching for. `tools/mcp-tools.test.ts` checks these
|
|
28
|
+
* against the workspace so they cannot quietly rot either.
|
|
29
|
+
*/
|
|
30
|
+
export const PREVIEW_GRID_WC_VERSION = '2.7.0';
|
|
31
|
+
const EXT_APPS_VERSION = '1.7.5';
|
|
32
|
+
const CDN = 'https://cdn.jsdelivr.net/npm';
|
|
33
|
+
/**
|
|
34
|
+
* The UI itself.
|
|
35
|
+
*
|
|
36
|
+
* Deliberately small and dependency-light: two module imports, one element, no
|
|
37
|
+
* framework. The iframe is sandboxed and the client enforces the CSP declared
|
|
38
|
+
* beside this resource, so anything else would just be blocked.
|
|
39
|
+
*/
|
|
40
|
+
const PREVIEW_HTML = `<!doctype html>
|
|
41
|
+
<html>
|
|
42
|
+
<head>
|
|
43
|
+
<meta charset="utf-8" />
|
|
44
|
+
<title>SvGrid preview</title>
|
|
45
|
+
<style>
|
|
46
|
+
html, body { margin: 0; height: 100%; font: 13px system-ui, sans-serif; }
|
|
47
|
+
#root { display: flex; flex-direction: column; height: 100%; min-height: 0; }
|
|
48
|
+
#note { padding: 8px 12px; color: #64748b; }
|
|
49
|
+
#host { flex: 1; min-width: 0; min-height: 0; }
|
|
50
|
+
sv-grid { display: block; height: 100%; }
|
|
51
|
+
</style>
|
|
52
|
+
</head>
|
|
53
|
+
<body>
|
|
54
|
+
<div id="root">
|
|
55
|
+
<div id="note">Loading the grid…</div>
|
|
56
|
+
<div id="host"></div>
|
|
57
|
+
</div>
|
|
58
|
+
<script type="module">
|
|
59
|
+
import { App } from '${CDN}/@modelcontextprotocol/ext-apps@${EXT_APPS_VERSION}/dist/src/app-with-deps.js'
|
|
60
|
+
import '${CDN}/@svgrid/grid-wc@${PREVIEW_GRID_WC_VERSION}/dist/sv-grid-element.js'
|
|
61
|
+
|
|
62
|
+
const note = document.getElementById('note')
|
|
63
|
+
const host = document.getElementById('host')
|
|
64
|
+
|
|
65
|
+
function render(payload) {
|
|
66
|
+
const { columns, data, title, ...rest } = payload ?? {}
|
|
67
|
+
if (!Array.isArray(columns) || !Array.isArray(data)) {
|
|
68
|
+
note.textContent = 'No grid data arrived from the tool.'
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
host.replaceChildren()
|
|
72
|
+
const el = document.createElement('sv-grid')
|
|
73
|
+
el.data = data
|
|
74
|
+
el.columns = columns
|
|
75
|
+
// Whatever else the tool passed - sortable, filterable, pageable,
|
|
76
|
+
// groupBy - goes straight through as a property, exactly as a host page
|
|
77
|
+
// would set it. Arrays and objects cannot cross as attributes, which is
|
|
78
|
+
// the whole reason the element takes properties.
|
|
79
|
+
for (const [key, value] of Object.entries(rest)) {
|
|
80
|
+
if (value !== undefined) el[key] = value
|
|
81
|
+
}
|
|
82
|
+
host.appendChild(el)
|
|
83
|
+
note.textContent = title
|
|
84
|
+
? title + ' - ' + data.length + ' rows'
|
|
85
|
+
: data.length + ' rows, ' + columns.length + ' columns'
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const app = new App()
|
|
90
|
+
app.ontoolresult = (params) => render(params?.structuredContent)
|
|
91
|
+
await app.connect()
|
|
92
|
+
} catch (err) {
|
|
93
|
+
note.textContent = 'Preview bridge unavailable: ' + (err && err.message ? err.message : err)
|
|
94
|
+
}
|
|
95
|
+
</script>
|
|
96
|
+
</body>
|
|
97
|
+
</html>
|
|
98
|
+
`;
|
|
99
|
+
export function previewResource() {
|
|
100
|
+
return {
|
|
101
|
+
uri: UI_RESOURCE_URI,
|
|
102
|
+
name: 'SvGrid preview',
|
|
103
|
+
title: 'SvGrid preview',
|
|
104
|
+
description: 'A live, interactive SvGrid rendered from the data a tool returns.',
|
|
105
|
+
mimeType: UI_MIME_TYPE,
|
|
106
|
+
_meta: {
|
|
107
|
+
// The iframe is offline-hostile by default; these are the only two
|
|
108
|
+
// origins it needs, and they are the ones the HTML imports from.
|
|
109
|
+
'ui/csp': { resourceDomains: [CDN.replace('https://', '').split('/')[0]] },
|
|
110
|
+
'ui/prefersBorder': true,
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
export function readPreviewResource(uri) {
|
|
115
|
+
if (uri !== UI_RESOURCE_URI)
|
|
116
|
+
return undefined;
|
|
117
|
+
return { contents: [{ uri, mimeType: UI_MIME_TYPE, text: PREVIEW_HTML }] };
|
|
118
|
+
}
|
|
119
|
+
export const PREVIEW_TOOL = {
|
|
120
|
+
name: 'svgrid_preview',
|
|
121
|
+
title: 'Preview a grid',
|
|
122
|
+
description: 'Render a REAL, interactive SvGrid in the conversation - sortable, filterable, scrollable - from columns and rows you pass, or from a demo id. Use it to show the user what a grid will look like before they build it, and after svgrid_check_code to demonstrate the result. In clients without UI support it returns a text summary instead, so it is always safe to call.',
|
|
123
|
+
inputSchema: {
|
|
124
|
+
type: 'object',
|
|
125
|
+
properties: {
|
|
126
|
+
columns: {
|
|
127
|
+
type: 'array',
|
|
128
|
+
description: 'Column definitions, e.g. [{ "field": "name", "header": "Name", "width": 160 }]. Required unless `demo` is set.',
|
|
129
|
+
},
|
|
130
|
+
data: {
|
|
131
|
+
type: 'array',
|
|
132
|
+
description: 'Rows to display. Required unless `demo` is set. Keep it under a few hundred for a preview.',
|
|
133
|
+
},
|
|
134
|
+
demo: {
|
|
135
|
+
type: 'string',
|
|
136
|
+
description: 'Preview a shipped demo instead, by id (e.g. "11-stock-market"). Its source is returned as text; find ids with svgrid_search.',
|
|
137
|
+
},
|
|
138
|
+
title: { type: 'string', description: 'Caption shown above the grid.' },
|
|
139
|
+
sortable: { type: 'boolean', description: 'Sortable headers. Default true.' },
|
|
140
|
+
filterable: { type: 'boolean', description: 'Filtering. Default true.' },
|
|
141
|
+
pageable: { type: 'boolean', description: 'Pagination footer.' },
|
|
142
|
+
groupBy: { type: 'array', description: 'Group by these column ids.' },
|
|
143
|
+
},
|
|
144
|
+
required: [],
|
|
145
|
+
},
|
|
146
|
+
_meta: { [UI_RESOURCE_META_KEY]: UI_RESOURCE_URI },
|
|
147
|
+
};
|
|
148
|
+
const fail = (message) => ({
|
|
149
|
+
isError: true,
|
|
150
|
+
content: [{ type: 'text', text: message }],
|
|
151
|
+
});
|
|
152
|
+
export function handlePreview(args) {
|
|
153
|
+
const demoId = typeof args.demo === 'string' ? args.demo.trim() : '';
|
|
154
|
+
if (demoId) {
|
|
155
|
+
const demo = examples.find((e) => e.id === demoId);
|
|
156
|
+
if (!demo) {
|
|
157
|
+
return fail(`No demo with id "${demoId}". Use svgrid_search to find one.`);
|
|
158
|
+
}
|
|
159
|
+
// A demo is a Svelte component, not data: there is nothing to hand the
|
|
160
|
+
// element. Return the source and say so, rather than rendering an empty
|
|
161
|
+
// grid and letting it look like the demo is broken.
|
|
162
|
+
return {
|
|
163
|
+
content: [
|
|
164
|
+
{
|
|
165
|
+
type: 'text',
|
|
166
|
+
text: `// ${demo.path}\n// ${demo.title} - ${demo.blurb}\n` +
|
|
167
|
+
`//\n// Demos are Svelte components, so this is the source rather than a live\n` +
|
|
168
|
+
`// render. To preview a grid interactively, pass \`columns\` and \`data\`.\n\n` +
|
|
169
|
+
demo.source,
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const columns = args.columns;
|
|
175
|
+
const data = args.data;
|
|
176
|
+
if (!Array.isArray(columns) || !columns.length) {
|
|
177
|
+
return fail('columns is required: an array of column definitions, or pass `demo` for a shipped example.');
|
|
178
|
+
}
|
|
179
|
+
if (!Array.isArray(data)) {
|
|
180
|
+
return fail('data is required: an array of rows.');
|
|
181
|
+
}
|
|
182
|
+
// A preview is a picture, not a dataset. `structuredContent` crosses into the
|
|
183
|
+
// conversation, so an unbounded `data` would put the caller's whole table in
|
|
184
|
+
// the context window - 200 rows is already ~2,900 tokens. Cap it and say so,
|
|
185
|
+
// rather than quietly rendering a truncated grid that looks complete.
|
|
186
|
+
const MAX_ROWS = 100;
|
|
187
|
+
const shown = data.slice(0, MAX_ROWS);
|
|
188
|
+
const truncated = data.length > shown.length;
|
|
189
|
+
const payload = { columns, data: shown };
|
|
190
|
+
for (const key of ['title', 'sortable', 'filterable', 'pageable', 'groupBy']) {
|
|
191
|
+
if (args[key] !== undefined)
|
|
192
|
+
payload[key] = args[key];
|
|
193
|
+
}
|
|
194
|
+
// The text half has to stand alone: a client without UI support shows only
|
|
195
|
+
// this, and "see the preview above" would be nonsense there.
|
|
196
|
+
const headers = columns
|
|
197
|
+
.map((c) => c.header ?? c.field ?? '?')
|
|
198
|
+
.join(', ');
|
|
199
|
+
const summary = `SvGrid preview: ${shown.length} row${shown.length === 1 ? '' : 's'} x ${columns.length} column${columns.length === 1 ? '' : 's'}` +
|
|
200
|
+
(typeof args.title === 'string' && args.title ? ` - ${args.title}` : '') +
|
|
201
|
+
`\nColumns: ${headers}` +
|
|
202
|
+
(truncated ? `\nShowing the first ${MAX_ROWS} of ${data.length} rows - a preview is a picture, not the dataset.` : '') +
|
|
203
|
+
`\n\nIn a client with MCP Apps support this renders as a live, interactive grid.`;
|
|
204
|
+
return {
|
|
205
|
+
content: [{ type: 'text', text: summary }],
|
|
206
|
+
// What the UI reads. Kept separate from the text so the model is not made
|
|
207
|
+
// to re-read the whole dataset it just sent.
|
|
208
|
+
structuredContent: payload,
|
|
209
|
+
_meta: { [UI_RESOURCE_META_KEY]: UI_RESOURCE_URI },
|
|
210
|
+
};
|
|
211
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slash-command style starters, exposed as MCP prompts.
|
|
3
|
+
*
|
|
4
|
+
* These are the tasks people actually arrive with, and a prompt is how a client
|
|
5
|
+
* surfaces one before the user has thought of a tool call. Each is written to
|
|
6
|
+
* make the model USE this server rather than recall SvGrid from training data,
|
|
7
|
+
* which is where the wrong-API answers come from - and every one of them ends
|
|
8
|
+
* at `svgrid_check_code`, because a grid that does not compile is the failure
|
|
9
|
+
* mode this whole server exists to prevent.
|
|
10
|
+
*/
|
|
11
|
+
export type Prompt = {
|
|
12
|
+
name: string;
|
|
13
|
+
title: string;
|
|
14
|
+
description: string;
|
|
15
|
+
arguments: {
|
|
16
|
+
name: string;
|
|
17
|
+
description: string;
|
|
18
|
+
required: boolean;
|
|
19
|
+
}[];
|
|
20
|
+
};
|
|
21
|
+
export declare const PROMPTS: Prompt[];
|
|
22
|
+
export declare function getPrompt(name: string, args: Record<string, unknown>): {
|
|
23
|
+
messages: {
|
|
24
|
+
role: "user";
|
|
25
|
+
content: {
|
|
26
|
+
type: "text";
|
|
27
|
+
text: string;
|
|
28
|
+
};
|
|
29
|
+
}[];
|
|
30
|
+
description: string;
|
|
31
|
+
} | undefined;
|
package/dist/prompts.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export const PROMPTS = [
|
|
2
|
+
{
|
|
3
|
+
name: 'build_grid',
|
|
4
|
+
title: 'Build a SvGrid',
|
|
5
|
+
description: 'Scaffold a SvGrid for a described dataset, grounded in the real API and verified before you see it.',
|
|
6
|
+
arguments: [
|
|
7
|
+
{ name: 'description', description: 'The data and behaviour you want, in plain language.', required: true },
|
|
8
|
+
],
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
name: 'explain_api',
|
|
12
|
+
title: 'Explain a SvGrid API',
|
|
13
|
+
description: 'Explain a prop, column option or method using the shipped docs rather than recall.',
|
|
14
|
+
arguments: [{ name: 'symbol', description: 'A prop, column option or method name.', required: true }],
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
name: 'review_grid_code',
|
|
18
|
+
title: 'Review SvGrid code',
|
|
19
|
+
description: 'Check existing SvGrid code against the real API surface and report what is wrong.',
|
|
20
|
+
arguments: [{ name: 'source', description: 'The component source to review.', required: true }],
|
|
21
|
+
},
|
|
22
|
+
];
|
|
23
|
+
const messages = (body) => ({
|
|
24
|
+
messages: [{ role: 'user', content: { type: 'text', text: body } }],
|
|
25
|
+
});
|
|
26
|
+
export function getPrompt(name, args) {
|
|
27
|
+
const arg = (key) => String(args[key] ?? '').trim();
|
|
28
|
+
if (name === 'build_grid') {
|
|
29
|
+
return {
|
|
30
|
+
description: 'Build a verified SvGrid',
|
|
31
|
+
...messages(`Build a SvGrid for: ${arg('description')}\n\n` +
|
|
32
|
+
'Work in this order, and do not skip a step:\n' +
|
|
33
|
+
'1. Call `svgrid_search` for the features this needs. Do not rely on memory of ' +
|
|
34
|
+
'SvGrid\'s API - it changes, and a wrong prop name fails silently.\n' +
|
|
35
|
+
'2. Call `svgrid_get` on the closest demo id and follow its structure.\n' +
|
|
36
|
+
'3. Write the component.\n' +
|
|
37
|
+
'4. Call `svgrid_check_code` on what you wrote. Use the `fixed` source it ' +
|
|
38
|
+
'returns rather than re-deriving the edits, and repeat until it is clean - ' +
|
|
39
|
+
'BEFORE showing me the code.\n' +
|
|
40
|
+
'5. Call `svgrid_preview` with the same columns and a few rows, so I can see ' +
|
|
41
|
+
'and touch the grid rather than just read it.'),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
if (name === 'explain_api') {
|
|
45
|
+
return {
|
|
46
|
+
description: 'Explain a SvGrid API from the shipped docs',
|
|
47
|
+
...messages(`Explain the SvGrid API \`${arg('symbol')}\`.\n\n` +
|
|
48
|
+
'Call `svgrid_search` with it first and answer only from what comes back. If it ' +
|
|
49
|
+
'is not in the results, say so plainly rather than guessing - a plausible ' +
|
|
50
|
+
'invented prop is worse than "not found".'),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
if (name === 'review_grid_code') {
|
|
54
|
+
return {
|
|
55
|
+
description: 'Review SvGrid code against the real API',
|
|
56
|
+
...messages('Review this SvGrid code:\n\n```svelte\n' +
|
|
57
|
+
arg('source') +
|
|
58
|
+
'\n```\n\n' +
|
|
59
|
+
'Call `svgrid_check_code` on it first. Explain each finding and how to fix it, ' +
|
|
60
|
+
'and if it returned a `fixed` source, show me that rather than describing the ' +
|
|
61
|
+
'edits. Use `svgrid_search` to back up any claim about what an API does.'),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type Resource = {
|
|
2
|
+
uri: string;
|
|
3
|
+
name: string;
|
|
4
|
+
title?: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
mimeType: string;
|
|
7
|
+
};
|
|
8
|
+
export declare function listResources(cursor?: string): {
|
|
9
|
+
resources: Resource[];
|
|
10
|
+
nextCursor?: string;
|
|
11
|
+
};
|
|
12
|
+
export type ResourceContents = {
|
|
13
|
+
contents: {
|
|
14
|
+
uri: string;
|
|
15
|
+
mimeType: string;
|
|
16
|
+
text: string;
|
|
17
|
+
}[];
|
|
18
|
+
};
|
|
19
|
+
export declare function readResource(uri: string): ResourceContents | undefined;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The docs and demo corpus as MCP resources.
|
|
3
|
+
*
|
|
4
|
+
* A resource is content the CLIENT can pull in and attach - the user picks it,
|
|
5
|
+
* rather than the model having to think of a tool call. That is the right shape
|
|
6
|
+
* for a fixed corpus of 408 pages and 375 demos, and it is the half of MCP this
|
|
7
|
+
* server never implemented: it advertised `{"tools":{}}` and answered
|
|
8
|
+
* `resources/list` with "Method not found".
|
|
9
|
+
*
|
|
10
|
+
* Listed lazily and by reference. `resources/list` returns names and URIs only,
|
|
11
|
+
* never content, so the listing stays cheap however large the corpus grows -
|
|
12
|
+
* the same reason the tools cap their listings.
|
|
13
|
+
*/
|
|
14
|
+
import { docs, examples } from './data.js';
|
|
15
|
+
import { previewResource } from './preview.js';
|
|
16
|
+
/** `svgrid://doc/help/columns/x` and `svgrid://example/11-stock-market`. */
|
|
17
|
+
function allResources() {
|
|
18
|
+
return [
|
|
19
|
+
// The preview UI belongs in the list, not bolted on beside it: prepending
|
|
20
|
+
// it to page one made that page 101 items, one over the page size, which is
|
|
21
|
+
// exactly the kind of off-by-one a paginating client trips on.
|
|
22
|
+
previewResource(),
|
|
23
|
+
...docs.map((d) => ({
|
|
24
|
+
uri: `svgrid://doc/${d.slug}`,
|
|
25
|
+
name: d.slug,
|
|
26
|
+
title: d.title,
|
|
27
|
+
description: `${d.section} - SvGrid documentation`,
|
|
28
|
+
mimeType: 'text/markdown',
|
|
29
|
+
})),
|
|
30
|
+
...examples.map((e) => ({
|
|
31
|
+
uri: `svgrid://example/${e.id}`,
|
|
32
|
+
name: e.id,
|
|
33
|
+
title: e.title,
|
|
34
|
+
description: `${e.category} - runnable SvGrid demo`,
|
|
35
|
+
mimeType: 'text/x-svelte',
|
|
36
|
+
})),
|
|
37
|
+
];
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* One page of resources.
|
|
41
|
+
*
|
|
42
|
+
* 783 resources in a single response is 147,604 chars - about 36,900 tokens,
|
|
43
|
+
* which is worse than the 36-tool listing this redesign set out to fix, and it
|
|
44
|
+
* lands in the client the moment anything enumerates resources. The protocol
|
|
45
|
+
* has cursor pagination for exactly this; the first version here did not use
|
|
46
|
+
* it, which is the sort of thing you only notice by measuring the payload.
|
|
47
|
+
*
|
|
48
|
+
* The cursor is just the offset. It is opaque to the client by contract, so
|
|
49
|
+
* there is no need for it to be anything cleverer, but it IS validated - a
|
|
50
|
+
* malformed cursor restarts from the beginning rather than throwing.
|
|
51
|
+
*/
|
|
52
|
+
const PAGE_SIZE = 100;
|
|
53
|
+
export function listResources(cursor) {
|
|
54
|
+
const all = allResources();
|
|
55
|
+
const start = Number.isSafeInteger(Number(cursor)) && Number(cursor) > 0 ? Number(cursor) : 0;
|
|
56
|
+
const page = all.slice(start, start + PAGE_SIZE);
|
|
57
|
+
const next = start + PAGE_SIZE;
|
|
58
|
+
return next < all.length ? { resources: page, nextCursor: String(next) } : { resources: page };
|
|
59
|
+
}
|
|
60
|
+
export function readResource(uri) {
|
|
61
|
+
const doc = /^svgrid:\/\/doc\/(.+)$/.exec(uri);
|
|
62
|
+
if (doc) {
|
|
63
|
+
const match = docs.find((d) => d.slug === doc[1]);
|
|
64
|
+
if (!match)
|
|
65
|
+
return undefined;
|
|
66
|
+
return { contents: [{ uri, mimeType: 'text/markdown', text: match.markdown }] };
|
|
67
|
+
}
|
|
68
|
+
const example = /^svgrid:\/\/example\/(.+)$/.exec(uri);
|
|
69
|
+
if (example) {
|
|
70
|
+
const match = examples.find((e) => e.id === example[1]);
|
|
71
|
+
if (!match)
|
|
72
|
+
return undefined;
|
|
73
|
+
return {
|
|
74
|
+
contents: [
|
|
75
|
+
{
|
|
76
|
+
uri,
|
|
77
|
+
mimeType: 'text/x-svelte',
|
|
78
|
+
text: `// ${match.path}\n// ${match.title} - ${match.blurb}\n\n${match.source}`,
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
package/dist/search.d.ts
CHANGED
|
@@ -21,6 +21,17 @@ export type DocHit = {
|
|
|
21
21
|
};
|
|
22
22
|
export declare function occurrences(haystack: string, needle: string): number;
|
|
23
23
|
/** Split a query into distinct lowercase terms, dropping one-character noise. */
|
|
24
|
+
/**
|
|
25
|
+
* Words too common to identify anything, dropped before matching.
|
|
26
|
+
*
|
|
27
|
+
* People phrase queries as questions - "how do I sort a column" - and every
|
|
28
|
+
* one of those filler words matched something. In the API search it was worse
|
|
29
|
+
* than noise: a single `a` matched 210 of 240 names, so the results came back
|
|
30
|
+
* led by `SvGridBoard` and `createSvGrid` for a query about pinning.
|
|
31
|
+
*/
|
|
32
|
+
export declare const STOP_WORDS: Set<string>;
|
|
33
|
+
/** Terms worth matching on: longer than one character, and not filler. */
|
|
34
|
+
export declare function meaningfulTerms(query: string): string[];
|
|
24
35
|
export declare function queryTokens(query: string): string[];
|
|
25
36
|
/** A window of text around the first needle that appears, for search results. */
|
|
26
37
|
export declare function excerptAround(markdown: string, needles: string[]): string;
|
package/dist/search.js
CHANGED
|
@@ -18,8 +18,36 @@ export function occurrences(haystack, needle) {
|
|
|
18
18
|
return n;
|
|
19
19
|
}
|
|
20
20
|
/** Split a query into distinct lowercase terms, dropping one-character noise. */
|
|
21
|
+
/**
|
|
22
|
+
* Words too common to identify anything, dropped before matching.
|
|
23
|
+
*
|
|
24
|
+
* People phrase queries as questions - "how do I sort a column" - and every
|
|
25
|
+
* one of those filler words matched something. In the API search it was worse
|
|
26
|
+
* than noise: a single `a` matched 210 of 240 names, so the results came back
|
|
27
|
+
* led by `SvGridBoard` and `createSvGrid` for a query about pinning.
|
|
28
|
+
*/
|
|
29
|
+
export const STOP_WORDS = new Set([
|
|
30
|
+
'a', 'an', 'and', 'are', 'as', 'at', 'be', 'by', 'can', 'do', 'does', 'for', 'from', 'how',
|
|
31
|
+
'i', 'in', 'is', 'it', 'my', 'of', 'on', 'or', 'the', 'to', 'use', 'want', 'what', 'when',
|
|
32
|
+
'where', 'which', 'with', 'you',
|
|
33
|
+
]);
|
|
34
|
+
/** Terms worth matching on: longer than one character, and not filler. */
|
|
35
|
+
export function meaningfulTerms(query) {
|
|
36
|
+
return [
|
|
37
|
+
...new Set(query
|
|
38
|
+
.toLowerCase()
|
|
39
|
+
.split(/[^a-z0-9]+/)
|
|
40
|
+
.filter((t) => t.length > 2 && !STOP_WORDS.has(t))),
|
|
41
|
+
];
|
|
42
|
+
}
|
|
21
43
|
export function queryTokens(query) {
|
|
22
44
|
const tokens = [...new Set(query.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 1))];
|
|
45
|
+
// Drop filler only when something specific survives - "do" is filler in "how
|
|
46
|
+
// do I sort", but a query that is ALL filler still has to match on something
|
|
47
|
+
// rather than silently matching everything.
|
|
48
|
+
const specific = tokens.filter((t) => !STOP_WORDS.has(t));
|
|
49
|
+
if (specific.length)
|
|
50
|
+
return specific;
|
|
23
51
|
return tokens.length ? tokens : [query.toLowerCase().trim()];
|
|
24
52
|
}
|
|
25
53
|
/** A window of text around the first needle that appears, for search results. */
|