@quasar/mcp 1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015-present Razvan Stoenescu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,67 @@
1
+ ![Quasar Framework logo](https://cdn.quasar.dev/logo-v2/header.png)
2
+
3
+ # @quasar/mcp
4
+
5
+ > An [MCP](https://modelcontextprotocol.io) server that gives AI agents the Quasar documentation and component API of the exact versions installed in your project.
6
+
7
+ <img alt="@quasar/mcp npm version" src="https://img.shields.io/npm/v/%40quasar/mcp.svg?label=@quasar/mcp">
8
+
9
+ [![mcp tests](https://github.com/quasarframework/quasar/actions/workflows/mcp-tests.yml/badge.svg?branch=dev)](https://github.com/quasarframework/quasar/actions/workflows/mcp-tests.yml)
10
+
11
+ The documentation pages ship inside the `quasar` and `@quasar/app-vite` packages (in their `dist/mcp` folder), and the component API inside `quasar` (`dist/api`), so the server works offline and always describes what you run. There is no fallback to quasar.dev: a project on releases predating the bundled docs gets the API only, and the server says so.
12
+
13
+ ## Setup
14
+
15
+ Add the server to your MCP client, from the project folder. Nothing gets installed in the project: `npx` starts the latest release when online and the cached copy when offline (the retries flag keeps that fallback quick):
16
+
17
+ ```json
18
+ {
19
+ "mcpServers": {
20
+ "quasar": {
21
+ "command": "npx",
22
+ "args": ["-y", "--fetch-retries=0", "@quasar/mcp@latest"]
23
+ }
24
+ }
25
+ }
26
+ ```
27
+
28
+ The server serves the project it is started in. Clients that start servers elsewhere can point it at the project with `--project <dir>`.
29
+
30
+ See the [AI Agents](https://quasar.dev/start/ai-agents) page for the per-client instructions.
31
+
32
+ ## Tools
33
+
34
+ | Tool | What it returns |
35
+ | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
36
+ | `list_pages` | every page available offline with its approximate size, grouped by the package shipping it |
37
+ | `search_docs` | the pages matching some keywords, with their description, approximate size and the sections the keywords occur in |
38
+ | `get_page` | one page, one section of it (by heading, or the `#anchor` of a pasted link), or its outline, as markdown |
39
+ | `list_api` | the names of the API descriptors: components, plugins, directives, utilities |
40
+ | `get_api` | the props, slots, events and methods of one of them, one part, or one member, as the site shows them; `format: "json"` for the raw descriptor |
41
+ | `check_updates` | whether newer releases of quasar, @quasar/app-vite or this server exist |
42
+
43
+ At session start the server also tells the agent which package versions it serves, what is missing, and which updates are available (checked in the background, cached for a day, never while offline, the same mechanism the Quasar CLI uses; `NO_UPDATE_NOTIFIER` disables it).
44
+
45
+ ## Chat Support
46
+
47
+ Ask questions at the official community Discord server: [https://chat.quasar.dev](https://chat.quasar.dev)
48
+
49
+ ## Community Forum
50
+
51
+ Ask questions at the official community forum: [https://forum.quasar.dev](https://forum.quasar.dev)
52
+
53
+ ## Contributing
54
+
55
+ I'm excited if you want to contribute to Quasar under any form (report bugs, write a plugin, fix an issue, write a new feature). Please read the [Contributing Guide](../CONTRIBUTING.md).
56
+
57
+ ## Semver
58
+
59
+ Quasar is following [Semantic Versioning 2.0](https://semver.org/).
60
+
61
+ The documentation slices the packages ship (`dist/mcp`) carry a `format` number in their `meta.json`. It moves only when the shape the server parses changes, and this server's major version tracks it: `@quasar/mcp@1` reads format 1. A project whose installed packages carry another format is told so at session start, with the server to run instead.
62
+
63
+ ## License
64
+
65
+ Copyright (c) 2026-present Razvan Stoenescu
66
+
67
+ [MIT License](http://en.wikipedia.org/wiki/MIT_License)
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@quasar/mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for AI agents: the Quasar documentation and component API, served from the packages installed in your project",
5
+ "keywords": [
6
+ "agent",
7
+ "ai",
8
+ "docs",
9
+ "mcp",
10
+ "model-context-protocol",
11
+ "quasar",
12
+ "vue"
13
+ ],
14
+ "homepage": "https://quasar.dev",
15
+ "bugs": "https://github.com/quasarframework/quasar/issues",
16
+ "license": "MIT",
17
+ "author": {
18
+ "name": "Razvan Stoenescu",
19
+ "email": "razvan.stoenescu@gmail.com",
20
+ "url": "https://github.com/quasarframework"
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/quasarframework/quasar",
25
+ "directory": "mcp"
26
+ },
27
+ "funding": {
28
+ "type": "github",
29
+ "url": "https://donate.quasar.dev"
30
+ },
31
+ "bin": {
32
+ "quasar-mcp": "./src/bin.js"
33
+ },
34
+ "files": [
35
+ "src",
36
+ "!src/**/*.test.js",
37
+ "!src/test"
38
+ ],
39
+ "type": "module",
40
+ "exports": "./src/index.js",
41
+ "publishConfig": {
42
+ "access": "public"
43
+ },
44
+ "dependencies": {
45
+ "@modelcontextprotocol/sdk": "^1.30.0",
46
+ "@quasar/update-notifier": "^1.1.0",
47
+ "zod": "^4.6.0"
48
+ },
49
+ "devDependencies": {
50
+ "vitest": "^5.0.1"
51
+ },
52
+ "engines": {
53
+ "node": ">= 22.0.0"
54
+ },
55
+ "scripts": {
56
+ "test": "pnpm test:unit && pnpm test:e2e",
57
+ "test:unit": "vitest run",
58
+ "pretest:e2e": "pnpm --filter quasar generate:mcp && pnpm --filter @quasar/app-vite generate:mcp",
59
+ "test:e2e": "vitest run -c vitest-e2e.config.js"
60
+ }
61
+ }
package/src/api.js ADDED
@@ -0,0 +1,272 @@
1
+ import { existsSync, readFileSync, readdirSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+
4
+ import { extractSection } from './docs.js'
5
+
6
+ /** The sections of an API descriptor `get_api` can serve on their own. */
7
+ export const API_PARTS = [
8
+ 'props',
9
+ 'slots',
10
+ 'events',
11
+ 'methods',
12
+ 'computedProps',
13
+ 'value',
14
+ 'arg',
15
+ 'modifiers',
16
+ 'injection',
17
+ 'quasarConfOptions'
18
+ ]
19
+
20
+ /**
21
+ * The heading(s) the docs renderer (docs/build/mcp/api/render.js)
22
+ * gives each part in `api/<Name>.md`.
23
+ */
24
+ const PART_HEADINGS = {
25
+ props: ['Props'],
26
+ slots: ['Slots', 'Scoped Slots'],
27
+ events: ['Events'],
28
+ methods: ['Methods'],
29
+ computedProps: ['Computed Props'],
30
+ value: ['Directive Value'],
31
+ arg: ['Directive Argument'],
32
+ modifiers: ['Directive Modifiers'],
33
+ injection: ['Vue Injection'],
34
+ quasarConfOptions: ['quasar.config.js Options']
35
+ }
36
+
37
+ /** The parts whose entries are named members `get_api` can serve one of. */
38
+ export const MEMBER_PARTS = [
39
+ 'props',
40
+ 'computedProps',
41
+ 'methods',
42
+ 'events',
43
+ 'slots',
44
+ 'modifiers'
45
+ ]
46
+
47
+ /**
48
+ * Member names compare without case or punctuation: `modelValue`,
49
+ * `model-value` and the rendered `@update:model-value` all meet.
50
+ *
51
+ * @param {string} value
52
+ * @returns {string}
53
+ */
54
+ function normalizeMember(value) {
55
+ return value.toLowerCase().replaceAll(/[^a-z0-9]/g, '')
56
+ }
57
+
58
+ const cache = new Map()
59
+
60
+ /**
61
+ * @param {string} apiDir
62
+ * @returns {string[]} Descriptor names (`QBtn`, `Notify`, `Ripple`, ...), sorted.
63
+ */
64
+ export function listApi(apiDir) {
65
+ return readdirSync(apiDir)
66
+ .filter(file => file.endsWith('.json'))
67
+ .map(file => file.slice(0, -'.json'.length))
68
+ .sort()
69
+ }
70
+
71
+ /**
72
+ * @param {string} apiDir
73
+ * @param {string} name
74
+ * @returns {Record<string, unknown> | null}
75
+ */
76
+ export function readApi(apiDir, name) {
77
+ const file = join(apiDir, `${name}.json`)
78
+ let api = cache.get(file)
79
+ if (api === void 0) {
80
+ try {
81
+ api = JSON.parse(readFileSync(file, 'utf8'))
82
+ } catch {
83
+ return null
84
+ }
85
+ cache.set(file, api)
86
+ }
87
+ return api
88
+ }
89
+
90
+ /**
91
+ * Case-insensitive lookup that also forgives a missing `Q` prefix, so
92
+ * `btn`, `qbtn` and `QBtn` all find QBtn.
93
+ *
94
+ * @param {string} apiDir
95
+ * @param {string} name
96
+ * @returns {string | null} The canonical descriptor name.
97
+ */
98
+ export function resolveApiName(apiDir, name) {
99
+ const names = listApi(apiDir)
100
+ const wanted = name.trim().toLowerCase()
101
+ return (
102
+ names.find(known => known.toLowerCase() === wanted) ??
103
+ names.find(known => known.toLowerCase() === `q${wanted}`) ??
104
+ null
105
+ )
106
+ }
107
+
108
+ /**
109
+ * @param {string} apiDir
110
+ * @param {string} name
111
+ * @returns {string[]} Names containing the input, for a miss.
112
+ */
113
+ export function similarApiNames(apiDir, name) {
114
+ const needle = name.trim().toLowerCase().replace(/^q/, '')
115
+ return listApi(apiDir)
116
+ .filter(known => known.toLowerCase().includes(needle))
117
+ .slice(0, 8)
118
+ }
119
+
120
+ /**
121
+ * The descriptor as the site inlines it (`api/<Name>.md` in the ui
122
+ * slice, rendered by the docs generator), whole or one part. Null when
123
+ * the installed release bundles no rendered form, or the part has no
124
+ * section there.
125
+ *
126
+ * @param {string} docsDir The ui slice (`dist/mcp`).
127
+ * @param {string} name
128
+ * @param {string} [part]
129
+ * @returns {string | null}
130
+ */
131
+ export function readApiMarkdown(docsDir, name, part) {
132
+ const markdown = readApiFile(docsDir, name)
133
+ if (markdown === null) {
134
+ return null
135
+ }
136
+ if (part === void 0) {
137
+ return markdown
138
+ }
139
+ const sections = PART_HEADINGS[part]
140
+ .map(heading => extractSection(markdown, heading))
141
+ .filter(section => section !== null)
142
+ return sections.length === 0 ? null : sections.join('\n\n') + '\n'
143
+ }
144
+
145
+ /**
146
+ * @param {string} docsDir
147
+ * @param {string} name
148
+ * @returns {string | null}
149
+ */
150
+ function readApiFile(docsDir, name) {
151
+ const file = join(docsDir, 'api', `${name}.md`)
152
+ let markdown = cache.get(file)
153
+ if (markdown === void 0) {
154
+ if (!existsSync(file)) {
155
+ return null
156
+ }
157
+ markdown = readFileSync(file, 'utf8')
158
+ cache.set(file, markdown)
159
+ }
160
+ return markdown
161
+ }
162
+
163
+ /**
164
+ * The members of a descriptor named like `member`, across the parts
165
+ * that have named members or in the one given.
166
+ *
167
+ * @param {Record<string, unknown>} api
168
+ * @param {string} member
169
+ * @param {string} [part]
170
+ * @returns {Array<{ part: string, name: string }>}
171
+ */
172
+ export function findApiMembers(api, member, part) {
173
+ const wanted = normalizeMember(member)
174
+ const matches = []
175
+ for (const candidate of part === void 0 ? MEMBER_PARTS : [part]) {
176
+ const entries = api[candidate]
177
+ if (!entries || typeof entries !== 'object') {
178
+ continue
179
+ }
180
+ for (const name of Object.keys(entries)) {
181
+ if (normalizeMember(name) === wanted) {
182
+ matches.push({ part: candidate, name })
183
+ }
184
+ }
185
+ }
186
+ return matches
187
+ }
188
+
189
+ /**
190
+ * @param {Record<string, unknown>} api
191
+ * @param {string} member
192
+ * @param {string} [part]
193
+ * @returns {string[]} `part.name` of the members containing the input, for a miss.
194
+ */
195
+ export function similarApiMembers(api, member, part) {
196
+ const needle = normalizeMember(member)
197
+ const similar = []
198
+ for (const candidate of part === void 0 ? MEMBER_PARTS : [part]) {
199
+ const entries = api[candidate]
200
+ if (!entries || typeof entries !== 'object') {
201
+ continue
202
+ }
203
+ for (const name of Object.keys(entries)) {
204
+ if (needle !== '' && normalizeMember(name).includes(needle)) {
205
+ similar.push(`${candidate}.${name}`)
206
+ }
207
+ }
208
+ }
209
+ return similar.slice(0, 8)
210
+ }
211
+
212
+ /**
213
+ * The name a rendered entry line (`- \`pagination\``, `- \`@click\``,
214
+ * `- \`#default\``, `- \`toggle(): void\``) is about, null for any
215
+ * other line.
216
+ *
217
+ * @param {string} line
218
+ * @returns {string | null}
219
+ */
220
+ function entryName(line) {
221
+ const match = /^- `([^`]+)`/.exec(line)
222
+ return match === null
223
+ ? null
224
+ : match[1].replace(/^[@#]/, '').replace(/\(.*$/, '')
225
+ }
226
+
227
+ /**
228
+ * The rendered entries of the given members, each under its section
229
+ * heading. Null when the release bundles no rendered form or none of
230
+ * the members has an entry there.
231
+ *
232
+ * @param {string} docsDir
233
+ * @param {string} name
234
+ * @param {Array<{ part: string, name: string }>} members From findApiMembers().
235
+ * @returns {string | null}
236
+ */
237
+ export function readApiMembersMarkdown(docsDir, name, members) {
238
+ const markdown = readApiFile(docsDir, name)
239
+ if (markdown === null) {
240
+ return null
241
+ }
242
+ const found = []
243
+ for (const member of members) {
244
+ const wanted = normalizeMember(member.name)
245
+ for (const heading of PART_HEADINGS[member.part]) {
246
+ const section = extractSection(markdown, heading)
247
+ if (section === null) {
248
+ continue
249
+ }
250
+ const [headingLine, ...lines] = section.split('\n')
251
+ let entry = null
252
+ for (const line of lines) {
253
+ const current = entryName(line)
254
+ if (current !== null) {
255
+ if (entry !== null) {
256
+ break
257
+ }
258
+ if (normalizeMember(current) === wanted) {
259
+ entry = [line]
260
+ }
261
+ } else if (entry !== null) {
262
+ entry.push(line)
263
+ }
264
+ }
265
+ if (entry !== null) {
266
+ found.push(`${headingLine}\n\n${entry.join('\n').trim()}`)
267
+ break
268
+ }
269
+ }
270
+ }
271
+ return found.length === 0 ? null : found.join('\n\n') + '\n'
272
+ }
package/src/bin.js ADDED
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { parseArgs } from 'node:util'
4
+
5
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
6
+
7
+ import { loadProject } from './project.js'
8
+ import { createServer } from './server.js'
9
+ import { version } from './version.js'
10
+
11
+ const usage = `Usage: quasar-mcp [--project <dir>]
12
+
13
+ MCP server (stdio) for the Quasar documentation and component API of the
14
+ packages installed in a project. Point your MCP client at it, e.g.:
15
+
16
+ { "mcpServers": { "quasar": { "command": "npx", "args": ["-y", "--fetch-retries=0", "@quasar/mcp@latest"] } } }
17
+
18
+ Options:
19
+ --project <dir> Project to serve, the one whose installed quasar and
20
+ @quasar/app-vite carry the docs (default: cwd; when
21
+ cwd has none, the first app found below it is served)
22
+ -v, --version Print the version and exit
23
+ -h, --help Print this help and exit
24
+ `
25
+
26
+ let values
27
+ try {
28
+ ;({ values } = parseArgs({
29
+ options: {
30
+ project: { type: 'string' },
31
+ version: { type: 'boolean', short: 'v' },
32
+ help: { type: 'boolean', short: 'h' }
33
+ },
34
+ strict: true
35
+ }))
36
+ } catch (err) {
37
+ console.error(err.message)
38
+ console.error(usage)
39
+ process.exit(1)
40
+ }
41
+
42
+ if (values.help === true) {
43
+ console.log(usage)
44
+ process.exit()
45
+ }
46
+
47
+ if (values.version === true) {
48
+ console.log(version)
49
+ process.exit()
50
+ }
51
+
52
+ // stdout carries the protocol: anything else a dependency might print
53
+ // there would corrupt the session, so route every console channel to
54
+ // stderr, which MCP clients keep as the server's log.
55
+ for (const channel of ['log', 'info', 'debug', 'table', 'dir']) {
56
+ console[channel] = console.error
57
+ }
58
+
59
+ const project = loadProject(values.project, {
60
+ explicit: values.project !== void 0
61
+ })
62
+ const server = await createServer({ project })
63
+ await server.connect(new StdioServerTransport())