@docsector/docsector-reader 1.4.0 → 1.6.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 +131 -0
- package/bin/docsector.js +33 -1
- package/package.json +1 -1
- package/src/App.vue +22 -3
- package/src/composables/useWebMcp.js +398 -0
- package/src/index.js +48 -0
- package/src/quasar.factory.js +131 -0
package/README.md
CHANGED
|
@@ -30,6 +30,8 @@ Transform Markdown content into beautiful, navigable documentation sites — wit
|
|
|
30
30
|
- 🤖 **AI-Friendly robots.txt** — Scaffold includes a `robots.txt` explicitly allowing 23 AI crawlers (GPTBot, ClaudeBot, PerplexityBot, GrokBot, etc.)
|
|
31
31
|
- 🧭 **Content Signals** — Optional `Content-Signal` directive for declaring AI usage policy (`ai-train`, `search`, `ai-input`) in `robots.txt`
|
|
32
32
|
- 🧩 **Agent Skills Discovery Index** — Optional `/.well-known/agent-skills/index.json` with RFC v0.2.0 schema and SHA-256 digests
|
|
33
|
+
- 🪪 **MCP Server Card** — Optional `/.well-known/mcp/server-card.json` for MCP server discovery before connection
|
|
34
|
+
- 🌐 **WebMCP Browser Tools** — Optional registration of in-page tools via `navigator.modelContext` for browser agents
|
|
33
35
|
- 🔗 **Homepage Link Headers** — Auto-generated `Link` response headers for agent discovery (`api-catalog`, `service-doc`, `service-desc`, `describedby`) per RFC 8288 / RFC 9727
|
|
34
36
|
- 🔌 **MCP Server** — Auto-generated [MCP](https://modelcontextprotocol.io) server at `/mcp` for AI assistant integration (Claude Desktop, VS Code, etc.)
|
|
35
37
|
- 📄 **llms.txt / llms-full.txt** — Auto-generated [llms.txt](https://llmstxt.org) index and full-content file for LLM discovery (requires `siteUrl` in config)
|
|
@@ -45,6 +47,7 @@ Transform Markdown content into beautiful, navigable documentation sites — wit
|
|
|
45
47
|
- 🌗 **Dark/Light Mode** — Automatic theme switching with Quasar Dark Plugin
|
|
46
48
|
- 🔗 **Anchor Navigation** — Right-side Table of Contents tree with scroll tracking and auto-scroll to active section
|
|
47
49
|
- 🔎 **Search** — Menu search across all documentation content and tags
|
|
50
|
+
- 🌐 **WebMCP Browser Tools** — Registers in-page tools for browser agents with `registerTool` and optional `provideContext` fallback
|
|
48
51
|
- 📱 **Responsive** — Mobile-friendly with collapsible sidebar and drawers
|
|
49
52
|
- 🏷️ **Status Badges** — Mark pages as `done`, `draft`, or `empty` with visual indicators
|
|
50
53
|
- ✏️ **Edit on GitHub** — Direct links to edit pages on your repository
|
|
@@ -141,6 +144,126 @@ curl -X POST http://localhost:8788/mcp \
|
|
|
141
144
|
|
|
142
145
|
---
|
|
143
146
|
|
|
147
|
+
## 🪪 MCP Server Card Discovery
|
|
148
|
+
|
|
149
|
+
Docsector Reader can publish an MCP Server Card at:
|
|
150
|
+
|
|
151
|
+
- `/.well-known/mcp/server-card.json`
|
|
152
|
+
|
|
153
|
+
This supports pre-connection MCP discovery, exposing:
|
|
154
|
+
|
|
155
|
+
- `serverInfo` (`name`, `version`)
|
|
156
|
+
- MCP transport endpoint (defaults to `/mcp`)
|
|
157
|
+
- `capabilities` for tools/resources/prompts
|
|
158
|
+
|
|
159
|
+
When MCP is enabled, tool capabilities are derived from the generated server:
|
|
160
|
+
|
|
161
|
+
- `search_{toolSuffix}`
|
|
162
|
+
- `get_page_{toolSuffix}`
|
|
163
|
+
|
|
164
|
+
### Configure
|
|
165
|
+
|
|
166
|
+
```javascript
|
|
167
|
+
export default {
|
|
168
|
+
// ...other config
|
|
169
|
+
|
|
170
|
+
mcp: {
|
|
171
|
+
serverName: 'my-docs',
|
|
172
|
+
toolSuffix: 'my_docs'
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
mcpServerCard: {
|
|
176
|
+
enabled: true,
|
|
177
|
+
path: '/.well-known/mcp/server-card.json',
|
|
178
|
+
transportEndpoint: '/mcp',
|
|
179
|
+
transportType: 'streamable-http',
|
|
180
|
+
protocolVersion: '2025-03-26',
|
|
181
|
+
capabilities: {
|
|
182
|
+
tools: { supported: true },
|
|
183
|
+
resources: { supported: false },
|
|
184
|
+
prompts: { supported: false }
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Validate
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
npx docsector build
|
|
194
|
+
cat dist/spa/.well-known/mcp/server-card.json
|
|
195
|
+
cat dist/spa/_headers
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
External validation:
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
curl -X POST https://isitagentready.com/api/scan \
|
|
202
|
+
-H 'Content-Type: application/json' \
|
|
203
|
+
-d '{"url":"https://YOUR-SITE.com"}'
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Check `checks.discovery.mcpServerCard.status` equals `"pass"`.
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## 🌐 WebMCP Browser Tools
|
|
211
|
+
|
|
212
|
+
Docsector Reader can register browser-side tools for agents when
|
|
213
|
+
`navigator.modelContext` is available (secure context required).
|
|
214
|
+
|
|
215
|
+
Default tools:
|
|
216
|
+
|
|
217
|
+
- `docs.search_docs` (bridges to MCP `search_{toolSuffix}`)
|
|
218
|
+
- `docs.get_page` (bridges to MCP `get_page_{toolSuffix}`)
|
|
219
|
+
- `docs.navigate_to` (SPA navigation)
|
|
220
|
+
- `docs.copy_current_page` (current page markdown URL/content)
|
|
221
|
+
|
|
222
|
+
### WebMCP Configure
|
|
223
|
+
|
|
224
|
+
```javascript
|
|
225
|
+
export default {
|
|
226
|
+
// ...other config
|
|
227
|
+
|
|
228
|
+
mcp: {
|
|
229
|
+
serverName: 'my-docs',
|
|
230
|
+
toolSuffix: 'my_docs'
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
webMcp: {
|
|
234
|
+
enabled: true,
|
|
235
|
+
apiMode: 'dual', // 'registerTool' | 'dual'
|
|
236
|
+
toolPrefix: 'docs',
|
|
237
|
+
bridgeEndpoint: '/mcp',
|
|
238
|
+
bridgeToMcp: true,
|
|
239
|
+
tools: {
|
|
240
|
+
searchDocs: true,
|
|
241
|
+
getPage: true,
|
|
242
|
+
navigateTo: true,
|
|
243
|
+
copyCurrentPage: true
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Notes:
|
|
250
|
+
|
|
251
|
+
- `apiMode: 'registerTool'` uses only `navigator.modelContext.registerTool()`.
|
|
252
|
+
- `apiMode: 'dual'` also attempts `provideContext` fallback when available.
|
|
253
|
+
- Registration happens on page load and is automatically cleaned up on unmount.
|
|
254
|
+
|
|
255
|
+
### WebMCP Validate
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
curl -X POST https://isitagentready.com/api/scan \
|
|
259
|
+
-H 'Content-Type: application/json' \
|
|
260
|
+
-d '{"url":"https://YOUR-SITE.com"}'
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Check `checks.discovery.webMcp.status` equals `"pass"`.
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
144
267
|
## � llms.txt (LLM Discovery)
|
|
145
268
|
|
|
146
269
|
Docsector Reader automatically generates [llms.txt](https://llmstxt.org) files at build time when `siteUrl` is configured (same requirement as sitemap.xml).
|
|
@@ -550,6 +673,14 @@ export default {
|
|
|
550
673
|
agentFallback: true
|
|
551
674
|
},
|
|
552
675
|
|
|
676
|
+
mcpServerCard: {
|
|
677
|
+
enabled: true,
|
|
678
|
+
path: '/.well-known/mcp/server-card.json',
|
|
679
|
+
transportEndpoint: '/mcp',
|
|
680
|
+
transportType: 'streamable-http',
|
|
681
|
+
protocolVersion: '2025-03-26'
|
|
682
|
+
},
|
|
683
|
+
|
|
553
684
|
contentSignals: {
|
|
554
685
|
enabled: true,
|
|
555
686
|
aiTrain: 'yes',
|
package/bin/docsector.js
CHANGED
|
@@ -23,7 +23,7 @@ const packageRoot = resolve(__dirname, '..')
|
|
|
23
23
|
const args = process.argv.slice(2)
|
|
24
24
|
const command = args[0]
|
|
25
25
|
|
|
26
|
-
const VERSION = '1.
|
|
26
|
+
const VERSION = '1.6.0'
|
|
27
27
|
|
|
28
28
|
const HELP = `
|
|
29
29
|
Docsector Reader v${VERSION}
|
|
@@ -133,6 +133,38 @@ export default {
|
|
|
133
133
|
// toolSuffix: 'my_docs'
|
|
134
134
|
// },
|
|
135
135
|
|
|
136
|
+
// @ MCP Server Card discovery (optional)
|
|
137
|
+
// Publishes /.well-known/mcp/server-card.json for pre-connection discovery.
|
|
138
|
+
// mcpServerCard: {
|
|
139
|
+
// enabled: true,
|
|
140
|
+
// path: '/.well-known/mcp/server-card.json',
|
|
141
|
+
// transportEndpoint: '/mcp',
|
|
142
|
+
// transportType: 'streamable-http',
|
|
143
|
+
// protocolVersion: '2025-03-26',
|
|
144
|
+
// capabilities: {
|
|
145
|
+
// tools: { supported: true },
|
|
146
|
+
// resources: { supported: false },
|
|
147
|
+
// prompts: { supported: false }
|
|
148
|
+
// }
|
|
149
|
+
// },
|
|
150
|
+
|
|
151
|
+
// @ WebMCP browser tools (optional)
|
|
152
|
+
// Registers tools in browser contexts that expose navigator.modelContext.
|
|
153
|
+
// Uses registerTool when available, with optional provideContext fallback.
|
|
154
|
+
// webMcp: {
|
|
155
|
+
// enabled: true,
|
|
156
|
+
// apiMode: 'dual', // 'registerTool' | 'dual'
|
|
157
|
+
// toolPrefix: 'docs',
|
|
158
|
+
// bridgeEndpoint: '/mcp',
|
|
159
|
+
// bridgeToMcp: true,
|
|
160
|
+
// tools: {
|
|
161
|
+
// searchDocs: true,
|
|
162
|
+
// getPage: true,
|
|
163
|
+
// navigateTo: true,
|
|
164
|
+
// copyCurrentPage: true
|
|
165
|
+
// }
|
|
166
|
+
// },
|
|
167
|
+
|
|
136
168
|
// @ Homepage Link headers for agent discovery (optional)
|
|
137
169
|
// linkHeaders: {
|
|
138
170
|
// enabled: true,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@docsector/docsector-reader",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "A documentation rendering engine built with Vue 3, Quasar v2 and Vite. Transform Markdown into beautiful, navigable documentation sites.",
|
|
5
5
|
"productName": "Docsector Reader",
|
|
6
6
|
"author": "Rodrigo de Araujo Vieira",
|
package/src/App.vue
CHANGED
|
@@ -114,20 +114,24 @@
|
|
|
114
114
|
</template>
|
|
115
115
|
|
|
116
116
|
<script setup>
|
|
117
|
-
import { reactive, computed, onMounted } from 'vue'
|
|
117
|
+
import { reactive, computed, onMounted, onBeforeUnmount } from 'vue'
|
|
118
118
|
import { useQuasar } from 'quasar'
|
|
119
119
|
import { useStore } from 'vuex'
|
|
120
120
|
import { useI18n } from 'vue-i18n'
|
|
121
|
-
import { useRouter } from 'vue-router'
|
|
121
|
+
import { useRouter, useRoute } from 'vue-router'
|
|
122
122
|
|
|
123
123
|
import docsectorConfig from 'docsector.config.js'
|
|
124
|
+
import { setupWebMcp } from './composables/useWebMcp'
|
|
124
125
|
|
|
125
126
|
defineOptions({ name: 'App' })
|
|
126
127
|
|
|
127
128
|
const $q = useQuasar()
|
|
128
129
|
const store = useStore()
|
|
129
|
-
const { locale } = useI18n()
|
|
130
|
+
const { locale, t } = useI18n()
|
|
130
131
|
const router = useRouter()
|
|
132
|
+
const route = useRoute()
|
|
133
|
+
|
|
134
|
+
let cleanupWebMcp = null
|
|
131
135
|
|
|
132
136
|
const settings = reactive({
|
|
133
137
|
general: {
|
|
@@ -213,6 +217,21 @@ onMounted(() => {
|
|
|
213
217
|
settings.appearance.background.default = dark
|
|
214
218
|
}
|
|
215
219
|
$q.dark.set(dark)
|
|
220
|
+
|
|
221
|
+
cleanupWebMcp = setupWebMcp({
|
|
222
|
+
router,
|
|
223
|
+
route,
|
|
224
|
+
store,
|
|
225
|
+
translate: t,
|
|
226
|
+
locale
|
|
227
|
+
})
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
onBeforeUnmount(() => {
|
|
231
|
+
if (typeof cleanupWebMcp === 'function') {
|
|
232
|
+
cleanupWebMcp()
|
|
233
|
+
cleanupWebMcp = null
|
|
234
|
+
}
|
|
216
235
|
})
|
|
217
236
|
</script>
|
|
218
237
|
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import docsectorConfig from 'docsector.config.js'
|
|
2
|
+
|
|
3
|
+
let activeCleanup = null
|
|
4
|
+
|
|
5
|
+
function toSafeToolPrefix (prefix) {
|
|
6
|
+
const value = String(prefix || 'docs')
|
|
7
|
+
.trim()
|
|
8
|
+
.replace(/[^A-Za-z0-9_.-]/g, '_')
|
|
9
|
+
|
|
10
|
+
if (!value) {
|
|
11
|
+
return 'docs'
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return value.slice(0, 48)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function decodeMarkdownSource (source) {
|
|
18
|
+
return String(source || '')
|
|
19
|
+
.replace(/{/g, '{')
|
|
20
|
+
.replace(/}/g, '}')
|
|
21
|
+
.replace(/\{'([^']+)'\}/g, '$1')
|
|
22
|
+
.replace(/&/g, '&')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isSecurePageContext () {
|
|
26
|
+
if (typeof window === 'undefined') return false
|
|
27
|
+
|
|
28
|
+
if (window.isSecureContext) return true
|
|
29
|
+
|
|
30
|
+
const protocol = window.location?.protocol || ''
|
|
31
|
+
const host = window.location?.hostname || ''
|
|
32
|
+
return protocol === 'https:' || host === 'localhost' || host === '127.0.0.1'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function buildMcpEndpoint (bridgeEndpoint) {
|
|
36
|
+
if (typeof window === 'undefined') return '/mcp'
|
|
37
|
+
|
|
38
|
+
const endpoint = bridgeEndpoint || '/mcp'
|
|
39
|
+
try {
|
|
40
|
+
return new URL(endpoint, window.location.origin).toString()
|
|
41
|
+
} catch {
|
|
42
|
+
return `${window.location.origin}/mcp`
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function callMcpTool ({ endpoint, toolName, args }) {
|
|
47
|
+
const requestId = `${toolName}:${Date.now()}`
|
|
48
|
+
const response = await fetch(endpoint, {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
headers: { 'Content-Type': 'application/json' },
|
|
51
|
+
body: JSON.stringify({
|
|
52
|
+
jsonrpc: '2.0',
|
|
53
|
+
id: requestId,
|
|
54
|
+
method: 'tools/call',
|
|
55
|
+
params: {
|
|
56
|
+
name: toolName,
|
|
57
|
+
arguments: args || {}
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
throw new Error(`MCP endpoint responded with ${response.status}`)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const payload = await response.json()
|
|
67
|
+
if (payload.error) {
|
|
68
|
+
throw new Error(payload.error.message || 'MCP tool call failed')
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return payload.result || null
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function normalizePath (inputPath) {
|
|
75
|
+
const value = String(inputPath || '').trim()
|
|
76
|
+
if (!value) return '/'
|
|
77
|
+
|
|
78
|
+
if (value.startsWith('http://') || value.startsWith('https://')) {
|
|
79
|
+
try {
|
|
80
|
+
const url = new URL(value)
|
|
81
|
+
return `${url.pathname}${url.search}${url.hash}`
|
|
82
|
+
} catch {
|
|
83
|
+
return '/'
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (value.startsWith('/')) {
|
|
88
|
+
return value
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return `/${value}`
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function createToolDefinitions ({
|
|
95
|
+
prefix,
|
|
96
|
+
mcpToolSuffix,
|
|
97
|
+
bridgeEndpoint,
|
|
98
|
+
bridgeToMcp,
|
|
99
|
+
tools,
|
|
100
|
+
router,
|
|
101
|
+
getCurrentPath,
|
|
102
|
+
getCurrentHash,
|
|
103
|
+
getLocale,
|
|
104
|
+
getAbsoluteI18nPath,
|
|
105
|
+
translate
|
|
106
|
+
}) {
|
|
107
|
+
const endpoint = buildMcpEndpoint(bridgeEndpoint)
|
|
108
|
+
|
|
109
|
+
const maybeCallMcp = async (toolName, args) => {
|
|
110
|
+
if (!bridgeToMcp) {
|
|
111
|
+
throw new Error('MCP bridge disabled in webMcp configuration')
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return callMcpTool({ endpoint, toolName, args })
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const definitions = []
|
|
118
|
+
|
|
119
|
+
if (tools.searchDocs) {
|
|
120
|
+
definitions.push({
|
|
121
|
+
name: `${prefix}.search_docs`,
|
|
122
|
+
description: 'Search documentation pages by keyword and return top matches.',
|
|
123
|
+
inputSchema: {
|
|
124
|
+
type: 'object',
|
|
125
|
+
properties: {
|
|
126
|
+
query: {
|
|
127
|
+
type: 'string',
|
|
128
|
+
description: 'Search term or phrase.'
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
required: ['query']
|
|
132
|
+
},
|
|
133
|
+
annotations: { readOnlyHint: true },
|
|
134
|
+
execute: async (input) => {
|
|
135
|
+
const query = String(input?.query || '').trim()
|
|
136
|
+
if (!query) {
|
|
137
|
+
return {
|
|
138
|
+
ok: false,
|
|
139
|
+
error: 'Missing required field: query'
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const toolName = `search_${mcpToolSuffix}`
|
|
144
|
+
const result = await maybeCallMcp(toolName, { query })
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
ok: true,
|
|
148
|
+
query,
|
|
149
|
+
mcpTool: toolName,
|
|
150
|
+
endpoint,
|
|
151
|
+
result
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (tools.getPage) {
|
|
158
|
+
definitions.push({
|
|
159
|
+
name: `${prefix}.get_page`,
|
|
160
|
+
description: 'Fetch a documentation page in markdown format by its path.',
|
|
161
|
+
inputSchema: {
|
|
162
|
+
type: 'object',
|
|
163
|
+
properties: {
|
|
164
|
+
path: {
|
|
165
|
+
type: 'string',
|
|
166
|
+
description: 'Documentation page path without trailing .md.'
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
required: ['path']
|
|
170
|
+
},
|
|
171
|
+
annotations: { readOnlyHint: true },
|
|
172
|
+
execute: async (input) => {
|
|
173
|
+
const path = String(input?.path || '').trim()
|
|
174
|
+
if (!path) {
|
|
175
|
+
return {
|
|
176
|
+
ok: false,
|
|
177
|
+
error: 'Missing required field: path'
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const toolName = `get_page_${mcpToolSuffix}`
|
|
182
|
+
const result = await maybeCallMcp(toolName, { path })
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
ok: true,
|
|
186
|
+
path,
|
|
187
|
+
mcpTool: toolName,
|
|
188
|
+
endpoint,
|
|
189
|
+
result
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (tools.navigateTo) {
|
|
196
|
+
definitions.push({
|
|
197
|
+
name: `${prefix}.navigate_to`,
|
|
198
|
+
description: 'Navigate to a docs route and optional anchor hash in the current session.',
|
|
199
|
+
inputSchema: {
|
|
200
|
+
type: 'object',
|
|
201
|
+
properties: {
|
|
202
|
+
path: {
|
|
203
|
+
type: 'string',
|
|
204
|
+
description: 'Target path (absolute or relative).'
|
|
205
|
+
},
|
|
206
|
+
hash: {
|
|
207
|
+
type: 'string',
|
|
208
|
+
description: 'Optional hash fragment, with or without leading #.'
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
annotations: { readOnlyHint: false },
|
|
213
|
+
execute: async (input) => {
|
|
214
|
+
const path = normalizePath(input?.path || getCurrentPath())
|
|
215
|
+
const hashInput = String(input?.hash || '').trim()
|
|
216
|
+
const hash = hashInput ? (hashInput.startsWith('#') ? hashInput : `#${hashInput}`) : ''
|
|
217
|
+
|
|
218
|
+
await router.push({ path, hash })
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
ok: true,
|
|
222
|
+
path,
|
|
223
|
+
hash,
|
|
224
|
+
currentPath: getCurrentPath(),
|
|
225
|
+
currentHash: getCurrentHash()
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (tools.copyCurrentPage) {
|
|
232
|
+
definitions.push({
|
|
233
|
+
name: `${prefix}.copy_current_page`,
|
|
234
|
+
description: 'Return markdown URL and markdown source for the current page context.',
|
|
235
|
+
inputSchema: {
|
|
236
|
+
type: 'object',
|
|
237
|
+
properties: {
|
|
238
|
+
includeContent: {
|
|
239
|
+
type: 'boolean',
|
|
240
|
+
description: 'When false, return metadata and URL only.'
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
annotations: { readOnlyHint: true },
|
|
245
|
+
execute: async (input) => {
|
|
246
|
+
const locale = getLocale() || 'en-US'
|
|
247
|
+
const currentPath = getCurrentPath()
|
|
248
|
+
const path = currentPath === '/' ? '/Homepage' : currentPath.replace(/\/+$/, '')
|
|
249
|
+
const markdownUrl = `${window.location.origin}${path}.md`
|
|
250
|
+
|
|
251
|
+
const includeContent = input?.includeContent !== false
|
|
252
|
+
const absolute = getAbsoluteI18nPath()
|
|
253
|
+
|
|
254
|
+
let content = ''
|
|
255
|
+
if (includeContent && absolute) {
|
|
256
|
+
const source = translate(`_.${absolute}.source`)
|
|
257
|
+
if (source) {
|
|
258
|
+
content = decodeMarkdownSource(source)
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
ok: true,
|
|
264
|
+
locale,
|
|
265
|
+
path: currentPath,
|
|
266
|
+
markdownUrl,
|
|
267
|
+
content
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
})
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return definitions
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function registerWithProvideContext (modelContext, toolDefinitions) {
|
|
277
|
+
if (typeof modelContext.provideContext !== 'function') return null
|
|
278
|
+
|
|
279
|
+
const payload = {
|
|
280
|
+
tools: toolDefinitions.map((tool) => ({
|
|
281
|
+
name: tool.name,
|
|
282
|
+
description: tool.description,
|
|
283
|
+
inputSchema: tool.inputSchema,
|
|
284
|
+
execute: tool.execute
|
|
285
|
+
}))
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const result = modelContext.provideContext(payload)
|
|
289
|
+
|
|
290
|
+
if (typeof result === 'function') {
|
|
291
|
+
return result
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (result && typeof result.dispose === 'function') {
|
|
295
|
+
return () => result.dispose()
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (result && typeof result.unregister === 'function') {
|
|
299
|
+
return () => result.unregister()
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return null
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function setupWebMcp ({ router, route, store, translate, locale }) {
|
|
306
|
+
const webMcp = docsectorConfig.webMcp || {}
|
|
307
|
+
if (!webMcp.enabled) return () => {}
|
|
308
|
+
|
|
309
|
+
if (!isSecurePageContext()) return () => {}
|
|
310
|
+
|
|
311
|
+
const modelContext = navigator?.modelContext
|
|
312
|
+
if (!modelContext) return () => {}
|
|
313
|
+
|
|
314
|
+
if (activeCleanup) {
|
|
315
|
+
activeCleanup()
|
|
316
|
+
activeCleanup = null
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const prefix = toSafeToolPrefix(webMcp.toolPrefix || 'docs')
|
|
320
|
+
const mcpToolSuffix = String(docsectorConfig.mcp?.toolSuffix || 'docs')
|
|
321
|
+
.replace(/[^A-Za-z0-9_]/g, '_')
|
|
322
|
+
|
|
323
|
+
const tools = {
|
|
324
|
+
searchDocs: webMcp.tools?.searchDocs !== false,
|
|
325
|
+
getPage: webMcp.tools?.getPage !== false,
|
|
326
|
+
navigateTo: webMcp.tools?.navigateTo !== false,
|
|
327
|
+
copyCurrentPage: webMcp.tools?.copyCurrentPage !== false
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const definitions = createToolDefinitions({
|
|
331
|
+
prefix,
|
|
332
|
+
mcpToolSuffix,
|
|
333
|
+
bridgeEndpoint: webMcp.bridgeEndpoint || '/mcp',
|
|
334
|
+
bridgeToMcp: webMcp.bridgeToMcp !== false,
|
|
335
|
+
tools,
|
|
336
|
+
router,
|
|
337
|
+
getCurrentPath: () => route.path,
|
|
338
|
+
getCurrentHash: () => route.hash,
|
|
339
|
+
getLocale: () => locale.value,
|
|
340
|
+
getAbsoluteI18nPath: () => store?.state?.i18n?.absolute,
|
|
341
|
+
translate
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
if (definitions.length === 0) return () => {}
|
|
345
|
+
|
|
346
|
+
const supportsRegisterTool = typeof modelContext.registerTool === 'function'
|
|
347
|
+
const supportsProvideContext = typeof modelContext.provideContext === 'function'
|
|
348
|
+
const mode = webMcp.apiMode === 'registerTool' ? 'registerTool' : 'dual'
|
|
349
|
+
|
|
350
|
+
const abortController = new AbortController()
|
|
351
|
+
const cleanups = [() => abortController.abort()]
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
if (supportsRegisterTool) {
|
|
355
|
+
for (const tool of definitions) {
|
|
356
|
+
modelContext.registerTool({
|
|
357
|
+
name: tool.name,
|
|
358
|
+
description: tool.description,
|
|
359
|
+
inputSchema: tool.inputSchema,
|
|
360
|
+
annotations: tool.annotations,
|
|
361
|
+
execute: tool.execute
|
|
362
|
+
}, { signal: abortController.signal })
|
|
363
|
+
}
|
|
364
|
+
} else if (mode === 'registerTool') {
|
|
365
|
+
return () => {}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if ((!supportsRegisterTool || mode === 'dual') && supportsProvideContext) {
|
|
369
|
+
const provideCleanup = registerWithProvideContext(modelContext, definitions)
|
|
370
|
+
if (provideCleanup) {
|
|
371
|
+
cleanups.push(provideCleanup)
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
} catch (error) {
|
|
375
|
+
if (import.meta.env?.DEV) {
|
|
376
|
+
console.warn('[docsector:webmcp] Failed to register tools', error)
|
|
377
|
+
}
|
|
378
|
+
return () => {}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const cleanup = () => {
|
|
382
|
+
while (cleanups.length > 0) {
|
|
383
|
+
const fn = cleanups.pop()
|
|
384
|
+
try {
|
|
385
|
+
fn()
|
|
386
|
+
} catch {
|
|
387
|
+
// Ignore cleanup errors
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
activeCleanup = cleanup
|
|
393
|
+
return cleanup
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export default {
|
|
397
|
+
setupWebMcp
|
|
398
|
+
}
|
package/src/index.js
CHANGED
|
@@ -78,6 +78,26 @@
|
|
|
78
78
|
* @param {string} [config.agentSkills.path='/.well-known/agent-skills/index.json'] - Output URI path for Agent Skills index
|
|
79
79
|
* @param {string} [config.agentSkills.schema='https://schemas.agentskills.io/discovery/0.2.0/schema.json'] - JSON Schema identifier for index payload
|
|
80
80
|
* @param {Array<{name:string,type:'skill-md'|'archive',description:string,url:string,digest?:string}>} [config.agentSkills.skills=[]] - Skills to publish in discovery index
|
|
81
|
+
* @param {Object} [config.mcpServerCard] - MCP Server Card discovery settings
|
|
82
|
+
* @param {boolean} [config.mcpServerCard.enabled=false] - Enables generation of MCP Server Card discovery document
|
|
83
|
+
* @param {string} [config.mcpServerCard.path='/.well-known/mcp/server-card.json'] - Output URI path for MCP Server Card
|
|
84
|
+
* @param {string} [config.mcpServerCard.transportEndpoint='/mcp'] - MCP transport endpoint exposed by the server
|
|
85
|
+
* @param {string} [config.mcpServerCard.transportType='streamable-http'] - Transport type label for discovery metadata
|
|
86
|
+
* @param {string} [config.mcpServerCard.protocolVersion='2025-03-26'] - Protocol version advertised by the server card
|
|
87
|
+
* @param {Object} [config.mcpServerCard.capabilities] - Optional capability overrides for tools/resources/prompts
|
|
88
|
+
* @param {Array<Object>} [config.mcpServerCard.remotes=[]] - Optional additional remotes to include in the server card
|
|
89
|
+
* @param {Object} [config.mcpServerCard.metadata] - Optional additional metadata merged into the server card payload
|
|
90
|
+
* @param {Object} [config.webMcp] - WebMCP browser tools settings
|
|
91
|
+
* @param {boolean} [config.webMcp.enabled=false] - Enables browser-side WebMCP tool registration on page load
|
|
92
|
+
* @param {'registerTool'|'dual'} [config.webMcp.apiMode='dual'] - Registration mode: registerTool only or registerTool + provideContext fallback
|
|
93
|
+
* @param {string} [config.webMcp.toolPrefix='docs'] - Prefix used to build WebMCP tool names (e.g. docs.search_docs)
|
|
94
|
+
* @param {string} [config.webMcp.bridgeEndpoint='/mcp'] - Relative endpoint used to bridge search/get_page to the MCP HTTP server
|
|
95
|
+
* @param {boolean} [config.webMcp.bridgeToMcp=true] - Uses MCP endpoint bridge for search/get_page tools when true
|
|
96
|
+
* @param {Object} [config.webMcp.tools] - Per-tool enable flags
|
|
97
|
+
* @param {boolean} [config.webMcp.tools.searchDocs=true] - Enables tool search_docs
|
|
98
|
+
* @param {boolean} [config.webMcp.tools.getPage=true] - Enables tool get_page
|
|
99
|
+
* @param {boolean} [config.webMcp.tools.navigateTo=true] - Enables tool navigate_to
|
|
100
|
+
* @param {boolean} [config.webMcp.tools.copyCurrentPage=true] - Enables tool copy_current_page
|
|
81
101
|
* @returns {Object} Resolved Docsector configuration
|
|
82
102
|
*/
|
|
83
103
|
export function createDocsector (config = {}) {
|
|
@@ -169,6 +189,34 @@ export function createDocsector (config = {}) {
|
|
|
169
189
|
schema: 'https://schemas.agentskills.io/discovery/0.2.0/schema.json',
|
|
170
190
|
skills: [],
|
|
171
191
|
...config.agentSkills
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
mcpServerCard: {
|
|
195
|
+
enabled: false,
|
|
196
|
+
path: '/.well-known/mcp/server-card.json',
|
|
197
|
+
transportEndpoint: '/mcp',
|
|
198
|
+
transportType: 'streamable-http',
|
|
199
|
+
protocolVersion: '2025-03-26',
|
|
200
|
+
capabilities: null,
|
|
201
|
+
remotes: [],
|
|
202
|
+
metadata: null,
|
|
203
|
+
...config.mcpServerCard
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
webMcp: {
|
|
207
|
+
enabled: false,
|
|
208
|
+
apiMode: 'dual',
|
|
209
|
+
toolPrefix: 'docs',
|
|
210
|
+
bridgeEndpoint: '/mcp',
|
|
211
|
+
bridgeToMcp: true,
|
|
212
|
+
...config.webMcp,
|
|
213
|
+
tools: {
|
|
214
|
+
searchDocs: true,
|
|
215
|
+
getPage: true,
|
|
216
|
+
navigateTo: true,
|
|
217
|
+
copyCurrentPage: true,
|
|
218
|
+
...(config.webMcp?.tools || {})
|
|
219
|
+
}
|
|
172
220
|
}
|
|
173
221
|
}
|
|
174
222
|
}
|
package/src/quasar.factory.js
CHANGED
|
@@ -757,6 +757,8 @@ function createMarkdownBuildPlugin (projectRoot) {
|
|
|
757
757
|
const contentSignalsEnabled = contentSignalsConfig.enabled === true
|
|
758
758
|
const agentSkillsConfig = config.agentSkills || {}
|
|
759
759
|
const agentSkillsEnabled = agentSkillsConfig.enabled === true
|
|
760
|
+
const mcpServerCardConfig = config.mcpServerCard || {}
|
|
761
|
+
const mcpServerCardEnabled = mcpServerCardConfig.enabled === true
|
|
760
762
|
|
|
761
763
|
const toUrl = (href) => {
|
|
762
764
|
if (!href) return null
|
|
@@ -1112,6 +1114,54 @@ export async function onRequest (context) {
|
|
|
1112
1114
|
}
|
|
1113
1115
|
}
|
|
1114
1116
|
|
|
1117
|
+
if (mcpServerCardEnabled) {
|
|
1118
|
+
if (!config.mcp) {
|
|
1119
|
+
console.warn('\x1b[33m[docsector]\x1b[0m Skipped MCP Server Card generation: mcp config is not enabled')
|
|
1120
|
+
} else {
|
|
1121
|
+
const mcpServerCardPath = mcpServerCardConfig.path || '/.well-known/mcp/server-card.json'
|
|
1122
|
+
const cardDistPath = normalizeLocalPath(mcpServerCardPath)
|
|
1123
|
+
|
|
1124
|
+
if (!cardDistPath) {
|
|
1125
|
+
console.warn(`\x1b[33m[docsector]\x1b[0m Skipped MCP Server Card generation: path must be a local URI path, got "${mcpServerCardPath}"`)
|
|
1126
|
+
} else {
|
|
1127
|
+
const cardHref = mcpServerCardPath.startsWith('/') ? mcpServerCardPath : `/${mcpServerCardPath}`
|
|
1128
|
+
const mcpServerName = config.mcp.serverName || config.branding?.name || 'docs'
|
|
1129
|
+
const mcpServerVersion = config.branding?.version || '1.0.0'
|
|
1130
|
+
const mcpToolSuffix = config.mcp.toolSuffix || 'docs'
|
|
1131
|
+
const transportEndpoint = mcpServerCardConfig.transportEndpoint || '/mcp'
|
|
1132
|
+
const endpoint = toUrl(transportEndpoint)
|
|
1133
|
+
|
|
1134
|
+
if (!endpoint) {
|
|
1135
|
+
console.warn('\x1b[33m[docsector]\x1b[0m Skipped MCP Server Card generation: unable to resolve transport endpoint URL')
|
|
1136
|
+
} else {
|
|
1137
|
+
const cardPayload = buildMcpServerCardPayload({
|
|
1138
|
+
config,
|
|
1139
|
+
mcpServerCardConfig,
|
|
1140
|
+
serverName: mcpServerName,
|
|
1141
|
+
serverVersion: mcpServerVersion,
|
|
1142
|
+
endpoint,
|
|
1143
|
+
toolSuffix: mcpToolSuffix
|
|
1144
|
+
})
|
|
1145
|
+
|
|
1146
|
+
const cardDir = resolve(distDir, cardDistPath, '..')
|
|
1147
|
+
mkdirSync(cardDir, { recursive: true })
|
|
1148
|
+
writeFileSync(
|
|
1149
|
+
resolve(distDir, cardDistPath),
|
|
1150
|
+
JSON.stringify(cardPayload, null, 2) + '\n'
|
|
1151
|
+
)
|
|
1152
|
+
console.log(`\x1b[36m[docsector]\x1b[0m Generated ${cardHref}`)
|
|
1153
|
+
|
|
1154
|
+
const headersWithServerCard = readFileSync(headersPath, 'utf-8')
|
|
1155
|
+
if (!headersWithServerCard.includes(cardHref)) {
|
|
1156
|
+
const serverCardHeaders = `${cardHref}\n Content-Type: application/json; charset=utf-8\n`
|
|
1157
|
+
writeFileSync(headersPath, headersWithServerCard.trimEnd() + '\n\n' + serverCardHeaders)
|
|
1158
|
+
console.log(`\x1b[36m[docsector]\x1b[0m Added _headers rule for ${cardHref}`)
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1115
1165
|
console.log(`\x1b[36m[docsector]\x1b[0m Added _headers rule for .md files`)
|
|
1116
1166
|
|
|
1117
1167
|
// Add homepage Link headers for agent discovery (RFC 8288 / RFC 9727)
|
|
@@ -1504,6 +1554,87 @@ function resolveAgentSkillArtifactPath (artifactUrl, { siteUrl, distDir }) {
|
|
|
1504
1554
|
return resolve(distDir, relativePath)
|
|
1505
1555
|
}
|
|
1506
1556
|
|
|
1557
|
+
function mergeObjects (base, override) {
|
|
1558
|
+
if (!override || typeof override !== 'object' || Array.isArray(override)) {
|
|
1559
|
+
return base
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
return { ...base, ...override }
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
function buildMcpServerCardPayload ({
|
|
1566
|
+
config,
|
|
1567
|
+
mcpServerCardConfig,
|
|
1568
|
+
serverName,
|
|
1569
|
+
serverVersion,
|
|
1570
|
+
endpoint,
|
|
1571
|
+
toolSuffix
|
|
1572
|
+
}) {
|
|
1573
|
+
const transportType = mcpServerCardConfig.transportType || 'streamable-http'
|
|
1574
|
+
const protocolVersion = mcpServerCardConfig.protocolVersion || '2025-03-26'
|
|
1575
|
+
const baseTools = [
|
|
1576
|
+
`search_${toolSuffix}`,
|
|
1577
|
+
`get_page_${toolSuffix}`
|
|
1578
|
+
]
|
|
1579
|
+
|
|
1580
|
+
const defaultCapabilities = {
|
|
1581
|
+
tools: {
|
|
1582
|
+
supported: true,
|
|
1583
|
+
names: baseTools
|
|
1584
|
+
},
|
|
1585
|
+
resources: {
|
|
1586
|
+
supported: false
|
|
1587
|
+
},
|
|
1588
|
+
prompts: {
|
|
1589
|
+
supported: false
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
const capabilitiesOverride = (mcpServerCardConfig.capabilities && typeof mcpServerCardConfig.capabilities === 'object' && !Array.isArray(mcpServerCardConfig.capabilities))
|
|
1594
|
+
? mcpServerCardConfig.capabilities
|
|
1595
|
+
: {}
|
|
1596
|
+
const capabilities = {
|
|
1597
|
+
...capabilitiesOverride,
|
|
1598
|
+
tools: mergeObjects(defaultCapabilities.tools, capabilitiesOverride.tools),
|
|
1599
|
+
resources: mergeObjects(defaultCapabilities.resources, capabilitiesOverride.resources),
|
|
1600
|
+
prompts: mergeObjects(defaultCapabilities.prompts, capabilitiesOverride.prompts)
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
const primaryRemote = {
|
|
1604
|
+
transportType,
|
|
1605
|
+
endpoint,
|
|
1606
|
+
supportedProtocolVersions: [protocolVersion]
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
const customRemotes = Array.isArray(mcpServerCardConfig.remotes)
|
|
1610
|
+
? mcpServerCardConfig.remotes
|
|
1611
|
+
: []
|
|
1612
|
+
const remotes = [primaryRemote, ...customRemotes]
|
|
1613
|
+
|
|
1614
|
+
const payload = {
|
|
1615
|
+
serverInfo: {
|
|
1616
|
+
name: serverName,
|
|
1617
|
+
version: serverVersion
|
|
1618
|
+
},
|
|
1619
|
+
title: config.branding?.name || serverName,
|
|
1620
|
+
description: config.branding?.description || null,
|
|
1621
|
+
transport: {
|
|
1622
|
+
type: transportType,
|
|
1623
|
+
endpoint
|
|
1624
|
+
},
|
|
1625
|
+
remotes,
|
|
1626
|
+
capabilities,
|
|
1627
|
+
tools: baseTools.map(name => ({ name }))
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
const metadata = mcpServerCardConfig.metadata
|
|
1631
|
+
if (metadata && typeof metadata === 'object' && !Array.isArray(metadata)) {
|
|
1632
|
+
return { ...payload, ...metadata }
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
return payload
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1507
1638
|
/**
|
|
1508
1639
|
* Create a complete Quasar configuration for a docsector-reader consumer project.
|
|
1509
1640
|
*
|