nuxt-ai-ready 1.5.8 β†’ 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.
Files changed (46) hide show
  1. package/README.md +2 -1
  2. package/dist/cli.mjs +26 -36
  3. package/dist/module.d.mts +14 -2
  4. package/dist/module.json +2 -2
  5. package/dist/module.mjs +222 -33
  6. package/dist/runtime/app/composables/webmcp.d.ts +24 -0
  7. package/dist/runtime/app/composables/webmcp.js +103 -0
  8. package/dist/runtime/app/plugins/webmcp.client.d.ts +2 -0
  9. package/dist/runtime/app/plugins/webmcp.client.js +59 -0
  10. package/dist/runtime/app/webmcp-config.d.ts +3 -0
  11. package/dist/runtime/app/webmcp-config.js +7 -0
  12. package/dist/runtime/mcp.d.ts +11 -5
  13. package/dist/runtime/mcp.js +2 -1
  14. package/dist/runtime/server/db/queries.d.ts +4 -4
  15. package/dist/runtime/server/mcp/tools/get-page-markdown.js +35 -0
  16. package/dist/runtime/server/mcp/tools/list-pages.js +11 -4
  17. package/dist/runtime/server/mcp/tools/search-pages.js +11 -4
  18. package/dist/runtime/server/middleware/markdown.js +35 -8
  19. package/dist/runtime/server/middleware/markdown.prerender.js +1 -1
  20. package/dist/runtime/server/plugins/mcp-data.d.ts +2 -0
  21. package/dist/runtime/server/plugins/mcp-data.js +22 -0
  22. package/dist/runtime/server/routes/__ai-ready/pages.get.d.ts +41 -0
  23. package/dist/runtime/server/routes/__ai-ready/pages.get.js +48 -0
  24. package/dist/runtime/server/utils/cloudflare.js +9 -6
  25. package/dist/runtime/server/utils/content-negotiation.d.ts +16 -0
  26. package/dist/runtime/server/utils/content-negotiation.js +23 -0
  27. package/dist/runtime/server/utils/llms-full.js +18 -25
  28. package/dist/runtime/server/utils/sitemap.d.ts +2 -2
  29. package/dist/runtime/server/utils/sitemap.js +269 -79
  30. package/dist/runtime/server/utils.d.ts +7 -1
  31. package/dist/runtime/server/utils.js +4 -3
  32. package/dist/runtime/site-tool-catalog.d.ts +31 -0
  33. package/dist/runtime/site-tool-catalog.js +52 -0
  34. package/dist/runtime/site-tool-config.d.ts +75 -0
  35. package/dist/runtime/site-tool-config.js +0 -0
  36. package/dist/runtime/types.d.ts +37 -1
  37. package/dist/runtime/webmcp-public.d.ts +4 -0
  38. package/dist/runtime/webmcp-public.js +3 -0
  39. package/dist/runtime/webmcp-site-tools.d.ts +26 -0
  40. package/dist/runtime/webmcp-site-tools.js +282 -0
  41. package/dist/runtime/webmcp.d.ts +163 -0
  42. package/dist/runtime/webmcp.js +59 -0
  43. package/dist/types.d.mts +3 -1
  44. package/mcp.d.ts +1 -1
  45. package/package.json +33 -27
  46. package/webmcp.d.ts +1 -0
package/README.md CHANGED
@@ -21,7 +21,8 @@ Nuxt AI Ready implements both. It converts your pages to markdown, generates llm
21
21
  - πŸ“ **[Nuxt Content](https://content.nuxt.com) Integration**: Routes backed by a page collection serve source markdown directly, skipping HTMLβ†’markdown conversion
22
22
  - πŸ“‘ **Content Signals**: Configure AI training/search/input permissions via [Nuxt Robots](https://nuxtseo.com/robots)
23
23
  - 🌐 **Sitemap Integration**: Index AI-allowed pages via [Nuxt Sitemap](https://nuxtseo.com/sitemap)
24
- - ⚑ **MCP Server**: `list_pages` and `search_pages` tools with FTS5 full-text search
24
+ - ⚑ **MCP Server**: List, search and read indexed pages through `@nuxtjs/mcp-toolkit`
25
+ - 🧩 **[WebMCP](https://nuxtseo.com/ai-ready/guides/webmcp)**: Give browser agents access to built-in site tools or your own tools with `useWebMcpTool()`
25
26
  - πŸ—„οΈ **Runtime Indexing**: Index pages on-demand without prerendering, with SQLite/D1/LibSQL support
26
27
  - πŸ”” **[IndexNow](https://nuxtseo.com/ai-ready/guides/indexnow)**: Instantly notify Bing, Yandex, and other search engines when pages change
27
28
  - 🌏 **[i18n Aware](https://nuxtseo.com/ai-ready/guides/i18n)**: Auto-detects [`@nuxtjs/i18n`](https://i18n.nuxtjs.org/) for hreflang `Link` headers, locale-tagged frontmatter, and an Available Languages section in `llms.txt`
package/dist/cli.mjs CHANGED
@@ -17,6 +17,19 @@ async function getSecret(cwd) {
17
17
  function authHeaders(secret) {
18
18
  return { Authorization: `Bearer ${secret}` };
19
19
  }
20
+ async function requireSecret(cwd, hint = "") {
21
+ const secret = await getSecret(cwd);
22
+ if (!secret) {
23
+ consola.error(`No secret found. Run \`nuxi dev\` or \`nuxi build\` first${hint}.`);
24
+ }
25
+ return secret;
26
+ }
27
+ async function fetchJson(url, init, errorLabel = "Failed") {
28
+ return fetch(url, init).then((r) => r.json()).catch((err) => {
29
+ consola.error(`${errorLabel}: ${err.message}`);
30
+ return null;
31
+ });
32
+ }
20
33
  const main = defineCommand({
21
34
  meta: {
22
35
  name: "nuxt-ai-ready",
@@ -44,17 +57,12 @@ const main = defineCommand({
44
57
  },
45
58
  async run({ args }) {
46
59
  const cwd = resolve(args.cwd || ".");
47
- const secret = await getSecret(cwd);
48
- if (!secret) {
49
- consola.error("No secret found. Run `nuxi dev` or `nuxi build` first to generate one.");
60
+ const secret = await requireSecret(cwd, " to generate one");
61
+ if (!secret)
50
62
  return;
51
- }
52
63
  const url = `${args.url}/__ai-ready/status`;
53
64
  consola.info(`Fetching status from ${args.url}...`);
54
- const res = await fetch(url, { headers: authHeaders(secret) }).then((r) => r.json()).catch((err) => {
55
- consola.error(`Failed to connect: ${err.message}`);
56
- return null;
57
- });
65
+ const res = await fetchJson(url, { headers: authHeaders(secret) }, "Failed to connect");
58
66
  if (!res)
59
67
  return;
60
68
  consola.box("AI Ready Status");
@@ -146,11 +154,9 @@ const main = defineCommand({
146
154
  },
147
155
  async run({ args }) {
148
156
  const cwd = resolve(args.cwd || ".");
149
- const secret = await getSecret(cwd);
150
- if (!secret) {
151
- consola.error("No secret found. Run `nuxi dev` or `nuxi build` first.");
157
+ const secret = await requireSecret(cwd);
158
+ if (!secret)
152
159
  return;
153
- }
154
160
  const params = new URLSearchParams();
155
161
  if (args.all) {
156
162
  params.set("all", "true");
@@ -159,10 +165,7 @@ const main = defineCommand({
159
165
  }
160
166
  const url = `${args.url}/__ai-ready/poll?${params}`;
161
167
  consola.info(`Triggering poll at ${args.url}...`);
162
- const res = await fetch(url, { method: "POST", headers: authHeaders(secret) }).then((r) => r.json()).catch((err) => {
163
- consola.error(`Failed: ${err.message}`);
164
- return null;
165
- });
168
+ const res = await fetchJson(url, { method: "POST", headers: authHeaders(secret) });
166
169
  if (!res)
167
170
  return;
168
171
  consola.success(`Indexed: ${colors.green(res.indexed?.toString() || "0")} pages`);
@@ -201,21 +204,16 @@ const main = defineCommand({
201
204
  },
202
205
  async run({ args }) {
203
206
  const cwd = resolve(args.cwd || ".");
204
- const secret = await getSecret(cwd);
205
- if (!secret) {
206
- consola.error("No secret found. Run `nuxi dev` or `nuxi build` first.");
207
+ const secret = await requireSecret(cwd);
208
+ if (!secret)
207
209
  return;
208
- }
209
210
  const params = new URLSearchParams();
210
211
  if (!args.clear) {
211
212
  params.set("clear", "false");
212
213
  }
213
214
  const url = `${args.url}/__ai-ready/restore?${params}`;
214
215
  consola.info(`Restoring database at ${args.url}...`);
215
- const res = await fetch(url, { method: "POST", headers: authHeaders(secret) }).then((r) => r.json()).catch((err) => {
216
- consola.error(`Failed: ${err.message}`);
217
- return null;
218
- });
216
+ const res = await fetchJson(url, { method: "POST", headers: authHeaders(secret) });
219
217
  if (!res)
220
218
  return;
221
219
  consola.success(`Restored: ${colors.green(res.restored?.toString() || "0")} pages`);
@@ -265,12 +263,9 @@ const main = defineCommand({
265
263
  params.set("ttl", args.ttl);
266
264
  const url = `${args.url}/__ai-ready/prune?${params}`;
267
265
  consola.info(`${args.dry ? "Previewing" : "Pruning"} stale routes at ${args.url}...`);
268
- const res = await fetch(url, {
266
+ const res = await fetchJson(url, {
269
267
  method: "POST",
270
268
  headers: secret ? authHeaders(secret) : void 0
271
- }).then((r) => r.json()).catch((err) => {
272
- consola.error(`Failed: ${err.message}`);
273
- return null;
274
269
  });
275
270
  if (!res)
276
271
  return;
@@ -315,20 +310,15 @@ const main = defineCommand({
315
310
  },
316
311
  async run({ args }) {
317
312
  const cwd = resolve(args.cwd || ".");
318
- const secret = await getSecret(cwd);
319
- if (!secret) {
320
- consola.error("No secret found. Run `nuxi dev` or `nuxi build` first.");
313
+ const secret = await requireSecret(cwd);
314
+ if (!secret)
321
315
  return;
322
- }
323
316
  const params = new URLSearchParams({
324
317
  limit: args.limit || "100"
325
318
  });
326
319
  const url = `${args.url}/__ai-ready/indexnow?${params}`;
327
320
  consola.info(`Triggering IndexNow sync at ${args.url}...`);
328
- const res = await fetch(url, { method: "POST", headers: authHeaders(secret) }).then((r) => r.json()).catch((err) => {
329
- consola.error(`Failed: ${err.message}`);
330
- return null;
331
- });
321
+ const res = await fetchJson(url, { method: "POST", headers: authHeaders(secret) });
332
322
  if (!res)
333
323
  return;
334
324
  if (res.success) {
package/dist/module.d.mts CHANGED
@@ -1,6 +1,8 @@
1
1
  import * as _nuxt_schema from '@nuxt/schema';
2
- import { LlmsTxtConfig, ModuleOptions } from '../dist/runtime/types.js';
2
+ import { LlmsTxtConfig, ContentNegotiationPolicy, ModuleOptions } from '../dist/runtime/types.js';
3
3
  export { ModuleOptions } from '../dist/runtime/types.js';
4
+ import { ResolvedWebMcpToolsConfig } from '../dist/runtime/site-tool-config.js';
5
+ export { GetPageMarkdownToolOptions, ListPagesToolOptions, McpSiteToolAttachmentOptions, SearchPagesToolOptions, SiteToolOptions, SiteToolsConfig, WebMcpSiteToolAttachmentOptions } from '../dist/runtime/site-tool-config.js';
4
6
 
5
7
  interface ParsedMarkdownResult {
6
8
  markdown: string;
@@ -11,6 +13,11 @@ interface ParsedMarkdownResult {
11
13
  updatedAt?: string;
12
14
  }
13
15
 
16
+ interface ResolvedWebMcpConfig {
17
+ tools: ResolvedWebMcpToolsConfig;
18
+ exposedTo?: string[];
19
+ }
20
+
14
21
  interface ModuleHooks {
15
22
  /**
16
23
  * Hook called when page markdown is generated during prerendering.
@@ -36,6 +43,7 @@ declare module '@nuxt/schema' {
36
43
  interface ModulePublicRuntimeConfig {
37
44
  debug: boolean;
38
45
  debugCron: boolean;
46
+ contentNegotiation: ContentNegotiationPolicy;
39
47
  version: string;
40
48
  mdreamOptions: ModuleOptions['mdreamOptions'];
41
49
  markdownCacheHeaders: Required<NonNullable<ModuleOptions['markdownCacheHeaders']>>;
@@ -67,7 +75,11 @@ interface ModulePublicRuntimeConfig {
67
75
  } | null;
68
76
  ftsTokenizer?: string;
69
77
  }
78
+ /** Runtime config exposed to the browser, only set when WebMCP is enabled. */
79
+ interface ModuleAppRuntimeConfig {
80
+ webmcp: ResolvedWebMcpConfig;
81
+ }
70
82
  declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
71
83
 
72
84
  export { _default as default };
73
- export type { ModuleHooks, ModulePublicRuntimeConfig };
85
+ export type { ModuleAppRuntimeConfig, ModuleHooks, ModulePublicRuntimeConfig };
package/dist/module.json CHANGED
@@ -4,9 +4,9 @@
4
4
  "nuxt": ">=4.0.0"
5
5
  },
6
6
  "configKey": "aiReady",
7
- "version": "1.5.8",
7
+ "version": "1.6.0",
8
8
  "builder": {
9
- "@nuxt/module-builder": "1.0.2",
9
+ "@nuxt/module-builder": "1.0.3",
10
10
  "unbuild": "3.6.1"
11
11
  }
12
12
  }
package/dist/module.mjs CHANGED
@@ -1,14 +1,14 @@
1
1
  import { createHash, randomBytes } from 'node:crypto';
2
2
  import { mkdir, writeFile, appendFile, stat, readdir, access, readFile } from 'node:fs/promises';
3
3
  import { join, dirname, relative, resolve } from 'node:path';
4
- import { useLogger, useNuxt, resolveFiles, addTypeTemplate, addTemplate, defineNuxtModule, createResolver, hasNuxtModule, addServerPlugin, addServerHandler, addPlugin } from '@nuxt/kit';
4
+ import { useLogger, useNuxt, hasNuxtModule, resolveFiles, addTypeTemplate, addTemplate, defineNuxtModule, createResolver, addServerPlugin, addServerHandler, addPlugin, addImports, extendRouteRules } from '@nuxt/kit';
5
5
  import defu from 'defu';
6
6
  import { installNuxtSiteConfig, useSiteConfig, withSiteUrl } from 'nuxt-site-config/kit';
7
7
  import { setupDevToolsUI } from 'nuxtseo-shared/devtools';
8
8
  import { resolveNuxtContentVersion } from 'nuxtseo-shared/kit';
9
9
  import { readPackageJSON, resolvePackageJSON } from 'pkg-types';
10
- import { parseSitemapXml } from '@nuxtjs/sitemap/utils';
11
10
  import { colorize } from 'consola/utils';
11
+ import { collectSitemap } from 'sitemapd/parse';
12
12
  import { withLeadingSlash, withBase, joinURL } from 'ufo';
13
13
  import { normalizePagePath, toMarkdownPath } from '../dist/runtime/markdown-path.js';
14
14
  import { toLogicalRoute, toDeployedRoute } from '../dist/runtime/route-path.js';
@@ -249,11 +249,13 @@ async function crawlSitemapEntries(state, nuxt, nitro, entries) {
249
249
  }
250
250
  async function crawlSitemapContent(state, nuxt, nitro, sitemapContent) {
251
251
  logger.debug(`Parsing sitemap XML (${sitemapContent.length} bytes)`);
252
- const result = await parseSitemapXml(sitemapContent).catch((e) => {
253
- logger.debug(`Skipping sitemap: ${e.message}`);
254
- return null;
255
- });
256
- const urls = result?.urls || [];
252
+ const result = await collectSitemap(sitemapContent);
253
+ if (result._tag !== "document" || result.document._tag !== "urlset") {
254
+ const issues = result.issues.map((issue) => issue.message).join("; ");
255
+ logger.debug(`Skipping sitemap: ${issues || "document is not a URL set"}`);
256
+ return 0;
257
+ }
258
+ const urls = result.document.entries;
257
259
  logger.debug(`Found ${urls.length} URLs in sitemap`);
258
260
  return crawlSitemapEntries(state, nuxt, nitro, urls);
259
261
  }
@@ -269,10 +271,8 @@ function includesSitemapRoot(sitemapName, routes) {
269
271
  function detectSitemapPrerender(sitemapName = "sitemap.xml") {
270
272
  const nuxt = useNuxt();
271
273
  const prerenderedRoutes = nuxt.options.nitro.prerender?.routes || [];
272
- const hasSitemapModule = nuxt.options._installedModules?.some(
273
- (m) => m.meta?.name === "@nuxtjs/sitemap"
274
- );
275
- let prerenderSitemap = hasSitemapModule || isNuxtGenerate() || includesSitemapRoot(sitemapName, prerenderedRoutes);
274
+ const hasSitemapModule = hasNuxtModule("@nuxtjs/sitemap", nuxt);
275
+ let prerenderSitemap = isNuxtGenerate() || includesSitemapRoot(sitemapName, prerenderedRoutes);
276
276
  if (resolveNitroPreset() === "vercel-edge")
277
277
  prerenderSitemap = true;
278
278
  const hasPrerender = !!(nuxt.options.nitro.prerender?.routes?.length || nuxt.options.nitro.prerender?.crawlLinks);
@@ -491,7 +491,48 @@ function setupPrerenderHandler(options, dbPath, siteInfo, llmsTxtConfig, indexNo
491
491
  });
492
492
  }
493
493
 
494
- function registerTypeTemplates(_ctx) {
494
+ const declarativeWebMcpTypes = `
495
+ import type { WebMcpToolsContext } from 'nuxt-ai-ready/webmcp'
496
+
497
+ declare module '#app' {
498
+ interface RuntimeNuxtHooks {
499
+ /** Mutate built-in browser tools and their registration options before registration. */
500
+ 'ai-ready:webmcp:tools': (context: WebMcpToolsContext) => void | Promise<void>
501
+ }
502
+ }
503
+
504
+ declare module '@vue/runtime-dom' {
505
+ interface HTMLAttributes {
506
+ /** Names the tool this form exposes to agents. */
507
+ toolname?: string
508
+ /** Describes what submitting this form does. */
509
+ tooldescription?: string
510
+ /** Describes this field as a tool parameter. */
511
+ toolparamdescription?: string
512
+ /** Submit the form as soon as an agent fills it in. */
513
+ toolautosubmit?: boolean | ''
514
+ }
515
+ }
516
+
517
+ declare global {
518
+ interface SubmitEvent {
519
+ /** Whether an agent triggered this submit through a tool call. */
520
+ readonly agentInvoked?: boolean
521
+ /** Return a result to the agent. Call preventDefault() first. */
522
+ respondWith?: (result: Promise<unknown>) => void
523
+ }
524
+
525
+ interface WebMcpToolEvent extends Event {
526
+ readonly toolName: string
527
+ }
528
+
529
+ interface WindowEventMap {
530
+ toolactivated: WebMcpToolEvent
531
+ toolcancel: WebMcpToolEvent
532
+ }
533
+ }
534
+ `;
535
+ function registerTypeTemplates(ctx) {
495
536
  addTypeTemplate({
496
537
  filename: "types/nuxt-ai-ready-augments.d.ts",
497
538
  getContents: () => `// Generated by nuxt-ai-ready
@@ -506,7 +547,7 @@ declare module 'nitropack/types' {
506
547
  'ai-ready:page:indexed': (context: PageIndexedContext) => void | Promise<void>
507
548
  }
508
549
  }
509
-
550
+ ${ctx.config.webmcp ? declarativeWebMcpTypes : ""}
510
551
  export {}
511
552
  `
512
553
  });
@@ -625,6 +666,26 @@ async function detectI18n(opts = {}) {
625
666
  return toRuntimeI18nConfig(auto);
626
667
  }
627
668
 
669
+ function hasConfiguredNuxtModule(modules, name) {
670
+ return modules.some((entry) => {
671
+ const module = Array.isArray(entry) ? entry[0] : entry;
672
+ if (typeof module === "string")
673
+ return module === name;
674
+ if ((typeof module === "function" || typeof module === "object" && module !== null) && "meta" in module)
675
+ return module.meta?.name === name;
676
+ return false;
677
+ });
678
+ }
679
+ function resolveMcpToolkitState(input) {
680
+ if (!input.installed)
681
+ return { _tag: "Absent" };
682
+ if (input.options === false || input.options?.enabled === false)
683
+ return { _tag: "Disabled" };
684
+ if (input.static || input.generating)
685
+ return { _tag: "Static" };
686
+ return { _tag: "Enabled", route: input.options?.route || "/mcp" };
687
+ }
688
+
628
689
  function escapeRegExp(value) {
629
690
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
630
691
  }
@@ -656,6 +717,110 @@ function ensureStaticHeader(contents, route, name, value) {
656
717
  return `${contents.slice(0, blockStart)}${prefix} ${name}: ${value}${eol}${contents.slice(blockStart)}`;
657
718
  }
658
719
 
720
+ const DEFAULT_MAX_OUTPUT_CHARS = 1500;
721
+ const DEFAULT_LIST_LIMIT = 20;
722
+ const DEFAULT_SEARCH_LIMIT = 10;
723
+ const MAX_TOOL_LIMIT = 50;
724
+ function parsePositiveInteger(value, fallback, name, warnings, max) {
725
+ if (value === void 0)
726
+ return fallback;
727
+ const integer = Math.trunc(value);
728
+ if (!Number.isFinite(value) || integer < 1) {
729
+ warnings.push(`\`aiReady.${name}\` must be a positive finite integer. Using ${fallback}.`);
730
+ return fallback;
731
+ }
732
+ if (max && integer > max) {
733
+ warnings.push(`\`aiReady.${name}\` cannot exceed ${max}. Using ${max}.`);
734
+ return max;
735
+ }
736
+ if (integer !== value)
737
+ warnings.push(`\`aiReady.${name}\` must be an integer. Using ${integer}.`);
738
+ return integer;
739
+ }
740
+ function resolveWebMcpAttachment(options, path, warnings) {
741
+ if (options?.enabled === false)
742
+ return { enabled: false };
743
+ return {
744
+ enabled: true,
745
+ maxOutputChars: parsePositiveInteger(
746
+ options?.maxOutputChars,
747
+ DEFAULT_MAX_OUTPUT_CHARS,
748
+ `${path}.webmcp.maxOutputChars`,
749
+ warnings
750
+ ),
751
+ exposedTo: options?.exposedTo === void 0 ? void 0 : [...options.exposedTo]
752
+ };
753
+ }
754
+ function resolveSiteToolsConfig(input) {
755
+ const warnings = [];
756
+ return {
757
+ config: {
758
+ listPages: {
759
+ defaultLimit: parsePositiveInteger(
760
+ input?.listPages?.defaultLimit,
761
+ DEFAULT_LIST_LIMIT,
762
+ "tools.listPages.defaultLimit",
763
+ warnings,
764
+ MAX_TOOL_LIMIT
765
+ ),
766
+ mcp: { enabled: input?.listPages?.mcp?.enabled !== false },
767
+ webmcp: resolveWebMcpAttachment(input?.listPages?.webmcp, "tools.listPages", warnings)
768
+ },
769
+ searchPages: {
770
+ defaultLimit: parsePositiveInteger(
771
+ input?.searchPages?.defaultLimit,
772
+ DEFAULT_SEARCH_LIMIT,
773
+ "tools.searchPages.defaultLimit",
774
+ warnings,
775
+ MAX_TOOL_LIMIT
776
+ ),
777
+ mcp: { enabled: input?.searchPages?.mcp?.enabled !== false },
778
+ webmcp: resolveWebMcpAttachment(input?.searchPages?.webmcp, "tools.searchPages", warnings)
779
+ },
780
+ getPageMarkdown: {
781
+ mcp: { enabled: input?.getPageMarkdown?.mcp?.enabled !== false },
782
+ webmcp: resolveWebMcpAttachment(input?.getPageMarkdown?.webmcp, "tools.getPageMarkdown", warnings)
783
+ }
784
+ },
785
+ warnings
786
+ };
787
+ }
788
+ function resolveWebMcpConfig(input, toolsConfig) {
789
+ if (!input)
790
+ return { _tag: "Disabled" };
791
+ const options = input === true ? {} : input;
792
+ const tools = {};
793
+ if (options.tools !== false) {
794
+ if (toolsConfig.listPages.webmcp.enabled) {
795
+ tools.listPages = {
796
+ defaultLimit: toolsConfig.listPages.defaultLimit,
797
+ maxOutputChars: toolsConfig.listPages.webmcp.maxOutputChars,
798
+ exposedTo: toolsConfig.listPages.webmcp.exposedTo
799
+ };
800
+ }
801
+ if (toolsConfig.searchPages.webmcp.enabled) {
802
+ tools.searchPages = {
803
+ defaultLimit: toolsConfig.searchPages.defaultLimit,
804
+ maxOutputChars: toolsConfig.searchPages.webmcp.maxOutputChars,
805
+ exposedTo: toolsConfig.searchPages.webmcp.exposedTo
806
+ };
807
+ }
808
+ if (toolsConfig.getPageMarkdown.webmcp.enabled) {
809
+ tools.getPageMarkdown = {
810
+ maxOutputChars: toolsConfig.getPageMarkdown.webmcp.maxOutputChars,
811
+ exposedTo: toolsConfig.getPageMarkdown.webmcp.exposedTo
812
+ };
813
+ }
814
+ }
815
+ return {
816
+ _tag: "Enabled",
817
+ config: {
818
+ tools,
819
+ exposedTo: options.exposedTo?.length ? [...options.exposedTo] : void 0
820
+ }
821
+ };
822
+ }
823
+
659
824
  const module$1 = defineNuxtModule({
660
825
  meta: {
661
826
  name: "nuxt-ai-ready",
@@ -715,13 +880,17 @@ const module$1 = defineNuxtModule({
715
880
  if (rawConfig.mdreamOptions?.preset) {
716
881
  logger.warn("`mdreamOptions.preset` is deprecated. Use `mdreamOptions: { minimal: true }` instead. See https://github.com/harlan-zw/nuxt-ai-ready/releases/tag/v1.0.0");
717
882
  }
883
+ const siteToolsResult = resolveSiteToolsConfig(config.tools);
884
+ for (const warning of siteToolsResult.warnings)
885
+ logger.warn(warning);
886
+ const siteToolsConfig = siteToolsResult.config;
887
+ const hasMcpSiteTools = Object.values(siteToolsConfig).some((tool) => tool.mcp.enabled);
718
888
  await installNuxtSiteConfig();
719
889
  const i18nConfig = await detectI18n({ autoI18n: config.autoI18n });
720
890
  if (i18nConfig) {
721
891
  logger.info(`i18n detected: ${i18nConfig.locales.length} locales (default: ${i18nConfig.defaultLocale}, strategy: ${i18nConfig.strategy})`);
722
892
  }
723
893
  const ftsTokenizer = i18nConfig && hasCjkLocale(i18nConfig) ? "trigram" : "unicode61 remove_diacritics 2";
724
- nuxt.options.nitro.alias = nuxt.options.nitro.alias || {};
725
894
  nuxt.options.alias["#ai-ready"] = resolve("./runtime");
726
895
  const preset = String(nuxt.options.nitro.preset || "");
727
896
  const isCloudflare = preset.startsWith("cloudflare");
@@ -755,7 +924,14 @@ const module$1 = defineNuxtModule({
755
924
  );
756
925
  }
757
926
  }
758
- if (nuxt.options.mcp !== false && !nuxt.options.mcp?.name) {
927
+ const mcpToolkitState = resolveMcpToolkitState({
928
+ installed: hasNuxtModule("@nuxtjs/mcp-toolkit") || hasConfiguredNuxtModule(nuxt.options.modules, "@nuxtjs/mcp-toolkit"),
929
+ options: nuxt.options.mcp,
930
+ static: nuxt.options.nitro.static === true,
931
+ generating: nuxt.options._generate === true
932
+ });
933
+ const mcpAvailable = mcpToolkitState._tag === "Enabled";
934
+ if (mcpAvailable && nuxt.options.mcp !== false && !nuxt.options.mcp?.name) {
759
935
  nuxt.options.mcp = nuxt.options.mcp || {};
760
936
  nuxt.options.mcp.name = useSiteConfig().name;
761
937
  }
@@ -780,7 +956,7 @@ const module$1 = defineNuxtModule({
780
956
  group.contentUsage = [`train-ai=${config.contentSignal.aiTrain ? "y" : "n"}`];
781
957
  groups.push(group);
782
958
  }
783
- registerTypeTemplates();
959
+ registerTypeTemplates({ config });
784
960
  const defaultLlmsTxtSections = [];
785
961
  const llmsFullRoute = withSiteUrl("llms-full.txt", { withBase: true });
786
962
  defaultLlmsTxtSections.push({
@@ -793,19 +969,19 @@ const module$1 = defineNuxtModule({
793
969
  }
794
970
  ]
795
971
  });
796
- const hasMCP = hasNuxtModule("@nuxtjs/mcp-toolkit");
797
- if (hasMCP) {
972
+ if (mcpAvailable) {
973
+ addServerPlugin(resolve("./runtime/server/plugins/mcp-data"));
798
974
  nuxt.hook("mcp:definitions:paths", (paths) => {
799
975
  const mcpRuntimeDir = resolve(`./runtime/server/mcp`);
800
976
  const mcpConfig = config.mcp || {};
801
- if (mcpConfig.tools !== false)
977
+ if (mcpConfig.tools !== false && hasMcpSiteTools)
802
978
  (paths.tools ||= []).push(`${mcpRuntimeDir}/tools`);
803
979
  if (mcpConfig.resources !== false)
804
980
  (paths.resources ||= []).push(`${mcpRuntimeDir}/resources`);
805
981
  });
806
982
  const mcpLink = {
807
983
  title: "MCP",
808
- href: withSiteUrl(nuxt.options.mcp !== false && nuxt.options.mcp?.route || "/mcp", { withBase: true }),
984
+ href: withSiteUrl(mcpToolkitState.route, { withBase: true }),
809
985
  description: "Model Context Protocol server endpoint for AI agent integration."
810
986
  };
811
987
  if (defaultLlmsTxtSections[0]) {
@@ -836,12 +1012,15 @@ const module$1 = defineNuxtModule({
836
1012
  const buildDbPath = join(nuxt.options.buildDir, ".data/ai-ready/build.db");
837
1013
  const runtimeSyncConfig = typeof config.runtimeSync === "object" ? config.runtimeSync : {};
838
1014
  const runtimeSyncEnabled = !!config.runtimeSync || !!config.cron;
1015
+ const webmcpResult = resolveWebMcpConfig(config.webmcp, siteToolsConfig);
1016
+ const webmcpConfig = webmcpResult._tag === "Enabled" ? webmcpResult.config : null;
839
1017
  const indexNow = config.indexNow === true ? createHash("sha256").update(useSiteConfig().url || "nuxt-ai-ready").digest("hex").slice(0, 32) : config.indexNow || process.env.NUXT_AI_READY_INDEX_NOW_KEY;
840
1018
  nuxt.hooks.hook("nuxt-seo-pro:modules", (modules) => {
841
1019
  const mod = modules.find((m) => m.name === "nuxt-ai-ready");
842
1020
  if (mod) {
843
1021
  mod.features = {
844
- mcp: hasMCP,
1022
+ mcp: mcpAvailable,
1023
+ webmcp: !!webmcpConfig,
845
1024
  runtimeSync: runtimeSyncEnabled,
846
1025
  cron: !!config.cron,
847
1026
  indexNow: !!indexNow,
@@ -868,10 +1047,12 @@ const module$1 = defineNuxtModule({
868
1047
  nuxt.hooks.hook("nitro:config", (nitroConfig) => {
869
1048
  nitroConfig.experimental = nitroConfig.experimental || {};
870
1049
  nitroConfig.experimental.asyncContext = true;
1050
+ nitroConfig.externals = nitroConfig.externals || {};
1051
+ nitroConfig.externals.inline = nitroConfig.externals.inline || [];
1052
+ nitroConfig.externals.inline.push("sitemapd");
871
1053
  const preset2 = String(nitroConfig.preset || "");
872
1054
  const isEdgePreset = ["cloudflare", "vercel-edge", "netlify-edge", "deno"].some((p) => preset2.startsWith(p));
873
1055
  if (!isEdgePreset) {
874
- nitroConfig.externals = nitroConfig.externals || {};
875
1056
  nitroConfig.externals.external = nitroConfig.externals.external || [];
876
1057
  nitroConfig.externals.external.push("mdream");
877
1058
  }
@@ -913,6 +1094,7 @@ const module$1 = defineNuxtModule({
913
1094
  }
914
1095
  }
915
1096
  nitroConfig.virtual = nitroConfig.virtual || {};
1097
+ nitroConfig.virtual["#ai-ready-virtual/site-tools.mjs"] = `export default ${JSON.stringify(siteToolsConfig)}`;
916
1098
  const markdownLinkAvailabilityPath = join(dirname(buildDbPath), MARKDOWN_LINK_AVAILABILITY_FILE);
917
1099
  nitroConfig.virtual["#ai-ready-virtual/read-page-data.mjs"] = nuxt.options.dev ? `
918
1100
  export async function readPageDataFromFilesystem() { return { pages: [], errorRoutes: [] } }
@@ -983,11 +1165,8 @@ export async function readPageDataFromFilesystem() {
983
1165
  nitroConfig.virtual["#ai-ready-virtual/page-data.mjs"] = `export const pages = []
984
1166
  export const errorRoutes = []`;
985
1167
  nitroConfig.virtual["#ai-ready-virtual/logger.mjs"] = `
986
- import { createConsola } from 'consola'
987
- export const logger = createConsola({
988
- defaults: { tag: 'nuxt-ai-ready' },
989
- level: ${config.debug ? 4 : 3},
990
- })
1168
+ import { createModuleLogger } from 'nuxtseo-shared/utils'
1169
+ export const logger = createModuleLogger('nuxt-ai-ready', ${!!config.debug})
991
1170
  `;
992
1171
  const providerMap = {
993
1172
  sqlite: "#ai-ready/server/db/drizzle/providers/sqlite",
@@ -1002,7 +1181,7 @@ export const logger = createConsola({
1002
1181
  nitroConfig.virtual["#ai-ready-virtual/db-schema.mjs"] = `export * from '${schemaPath}'`;
1003
1182
  nitroConfig.virtual["#ai-ready-virtual/devtools-meta.mjs"] = `export const devtoolsMeta = ${JSON.stringify({
1004
1183
  contentSignal: config.contentSignal || false,
1005
- mcp: { enabled: hasMCP, tools: hasMCP && config.mcp?.tools !== false, resources: hasMCP && config.mcp?.resources !== false },
1184
+ mcp: { enabled: mcpAvailable, tools: mcpAvailable && config.mcp?.tools !== false && hasMcpSiteTools, resources: mcpAvailable && config.mcp?.resources !== false },
1006
1185
  cron: !!config.cron
1007
1186
  })}`;
1008
1187
  nitroConfig.virtual["#ai-ready-virtual/content-lookup.mjs"] = hasNuxtContentV3 ? `
@@ -1039,6 +1218,7 @@ export async function lookupContentPage(event, path) {
1039
1218
  version: version || "0.0.0",
1040
1219
  debug: config.debug || false,
1041
1220
  debugCron: config.debugCron || false,
1221
+ contentNegotiation: config.contentNegotiation === void 0 ? "auto" : config.contentNegotiation ? "enabled" : "disabled",
1042
1222
  mdreamOptions: config.mdreamOptions || {},
1043
1223
  markdownCacheHeaders: defu(config.markdownCacheHeaders, {
1044
1224
  maxAge: 3600,
@@ -1081,6 +1261,19 @@ export async function lookupContentPage(event, path) {
1081
1261
  });
1082
1262
  addServerHandler({ route: "/llms.txt", handler: resolve("./runtime/server/routes/llms.txt.get") });
1083
1263
  addServerHandler({ route: "/llms-full.txt", handler: resolve("./runtime/server/routes/llms-full.txt.get") });
1264
+ if (webmcpConfig) {
1265
+ addImports(["useWebMcpSupported", "useWebMcpTool"].map((name) => ({
1266
+ name,
1267
+ from: resolve("./runtime/app/composables/webmcp")
1268
+ })));
1269
+ nuxt.options.runtimeConfig.public["nuxt-ai-ready"] = {
1270
+ webmcp: webmcpConfig
1271
+ };
1272
+ addPlugin({ mode: "client", src: resolve("./runtime/app/plugins/webmcp.client") });
1273
+ if (Object.keys(webmcpConfig.tools).length) {
1274
+ addServerHandler({ route: "/__ai-ready/pages", handler: resolve("./runtime/server/routes/__ai-ready/pages.get") });
1275
+ }
1276
+ }
1084
1277
  addServerHandler({ route: "/__ai-ready__/debug.json", handler: resolve("./runtime/server/routes/__ai-ready/devtools.get") });
1085
1278
  if (config.debug) {
1086
1279
  addServerHandler({ route: "/__ai-ready-debug", handler: resolve("./runtime/server/routes/__ai-ready-debug.get") });
@@ -1128,12 +1321,8 @@ export async function lookupContentPage(event, path) {
1128
1321
  title: "AI Ready",
1129
1322
  icon: "carbon:ai-label"
1130
1323
  }, resolve, nuxt);
1131
- nuxt.options.nitro.routeRules = nuxt.options.nitro.routeRules || {};
1132
1324
  for (const route of ["/llms.txt", "/llms-full.txt"]) {
1133
- nuxt.options.nitro.routeRules[route] = defu(
1134
- nuxt.options.nitro.routeRules[route],
1135
- { headers: { "Content-Type": "text/plain; charset=utf-8" } }
1136
- );
1325
+ extendRouteRules(route, { headers: { "Content-Type": "text/plain; charset=utf-8" } });
1137
1326
  }
1138
1327
  nuxt.hooks.hook("nitro:build:before", (nitro) => {
1139
1328
  nitro.hooks.hook("compiled", async () => {
@@ -0,0 +1,24 @@
1
+ import type { ComputedRef, MaybeRefOrGetter, ShallowRef } from 'vue';
2
+ import type { InferWebMcpInput, WebMcpInputSchema, WebMcpRegisterOptions, WebMcpTool, WebMcpToolRegistrationState } from '../../webmcp.js';
3
+ export interface UseWebMcpToolOptions extends WebMcpRegisterOptions {
4
+ /** Register only while this reactive value is true. */
5
+ enabled?: MaybeRefOrGetter<boolean>;
6
+ }
7
+ export interface UseWebMcpToolReturn {
8
+ /** Whether the mounted client supports `document.modelContext`. */
9
+ supported: ComputedRef<boolean>;
10
+ /** Registration lifecycle, including browser rejections. */
11
+ state: Readonly<ShallowRef<WebMcpToolRegistrationState>>;
12
+ /** Remove the tool permanently for this composable instance. */
13
+ unregister: () => void;
14
+ }
15
+ /**
16
+ * Hydration-safe WebMCP support. It stays false through SSR and the first client
17
+ * render, then updates after mount.
18
+ */
19
+ export declare function useWebMcpSupported(): ComputedRef<boolean>;
20
+ /**
21
+ * Register a WebMCP tool while its component is mounted, active and enabled.
22
+ */
23
+ export declare function useWebMcpTool<const Schema extends WebMcpInputSchema, Output>(tool: WebMcpTool<InferWebMcpInput<NoInfer<Schema>>, Output, Schema>, options?: UseWebMcpToolOptions): UseWebMcpToolReturn;
24
+ export declare function useWebMcpTool<Input extends Record<string, unknown>, Output>(tool: WebMcpTool<Input, Output>, options?: UseWebMcpToolOptions): UseWebMcpToolReturn;