@farming-labs/docs 0.2.52 → 0.2.54

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 (51) hide show
  1. package/dist/agent-BXvi0uhS.mjs +623 -0
  2. package/dist/{agent-CaOBIVFy.mjs → agent-DXkXi-pS.mjs} +765 -22
  3. package/dist/agent-evals-BD17jOqL.mjs +1166 -0
  4. package/dist/agent-export-n-a0GbeU.mjs +854 -0
  5. package/dist/agent-scope-B8lptqCd.mjs +283 -0
  6. package/dist/agent-surface-drift-LS8zQgbq.mjs +1429 -0
  7. package/dist/{agents-DjhjZaNP.mjs → agents-BJaRQimo.mjs} +7 -5
  8. package/dist/{analytics-BZEwTK-8.mjs → analytics-Bx44lg6d.mjs} +1 -1
  9. package/dist/cli/index.mjs +43 -28
  10. package/dist/client/react.d.mts +1 -1
  11. package/dist/client/react.mjs +1 -1
  12. package/dist/{cloud-C_Ok9rC5.mjs → cloud-HWFlbfLJ.mjs} +4 -4
  13. package/dist/{cloud-ask-ai-Cl-xaV8j.mjs → cloud-ask-ai-1k0q6OAH.mjs} +1 -1
  14. package/dist/{cloud-ask-ai-DcMe6xOf.d.mts → cloud-ask-ai-466g6uAi.d.mts} +1 -1
  15. package/dist/code-blocks-C9awWzEQ.mjs +871 -0
  16. package/dist/codeblocks-BzfkREEC.mjs +250 -0
  17. package/dist/{config-tiQCZ46q.mjs → config-CydaZ5PB.mjs} +52 -11
  18. package/dist/{dev-Tm9Ss4D6.mjs → dev-BA2kRuCn.mjs} +3 -3
  19. package/dist/docs-cloud-server.d.mts +2 -2
  20. package/dist/docs-cloud-server.mjs +2 -2
  21. package/dist/{doctor-O9o9kjN8.mjs → doctor-CQpQ2zZm.mjs} +177 -47
  22. package/dist/{downgrade-CYEaOctn.mjs → downgrade-BStNhyCc.mjs} +2 -2
  23. package/dist/i18n-CAlj1ADU.mjs +40 -0
  24. package/dist/index.d.mts +158 -3
  25. package/dist/index.mjs +7 -6
  26. package/dist/{init-Ch5On0zm.mjs → init-gnHQ_Pz9.mjs} +3 -3
  27. package/dist/{mcp-DuCPNHS-.mjs → mcp-DnxZZWpY.mjs} +10 -5
  28. package/dist/mcp.d.mts +122 -6
  29. package/dist/mcp.mjs +1943 -86
  30. package/dist/{package-version-C8PigBZk.mjs → package-version-DRIc08EU.mjs} +1 -1
  31. package/dist/{reading-time-CPAy1SWO.mjs → reading-time-BrTd3DIh.mjs} +277 -33
  32. package/dist/{review-lm3dt7yy.mjs → review-B6gyEFkD.mjs} +184 -14
  33. package/dist/{robots-DV7u4Ire.mjs → robots-4BUJxlvV.mjs} +4 -4
  34. package/dist/{robots-XVPs9lVz.mjs → robots-DAptQnkx.mjs} +1 -1
  35. package/dist/{search-BWuU70B0.mjs → search-CJIC1Cqo.mjs} +173 -79
  36. package/dist/{search-B6nEkx86.mjs → search-CbPm2x5g.mjs} +6 -4
  37. package/dist/{search-DFEhf9-s.d.mts → search-CfWvmVYA.d.mts} +3 -1
  38. package/dist/server.d.mts +108 -5
  39. package/dist/server.mjs +9 -8
  40. package/dist/{sitemap-BEiKy4Iy.mjs → sitemap-D6nP3J1Q.mjs} +8 -6
  41. package/dist/{sitemap-server-xEHtDUDB.mjs → sitemap-server-wsNLyVkb.mjs} +1 -1
  42. package/dist/{templates-uaauJcTO.mjs → templates-1Cod8KrJ.mjs} +11 -5
  43. package/dist/{types-XqGLsmOD.d.mts → types-BVgucdVm.d.mts} +194 -2
  44. package/dist/{upgrade-DzQtpgJM.mjs → upgrade-CXYRNV0C.mjs} +2 -2
  45. package/package.json +1 -1
  46. package/dist/agent-DXDOMJBw.mjs +0 -9
  47. package/dist/codeblocks-z9iT794h.mjs +0 -1713
  48. package/dist/review-B7goPYUb.mjs +0 -541
  49. /package/dist/{cloud-analytics-Dlk_byos.mjs → cloud-analytics-CSyFE6SS.mjs} +0 -0
  50. /package/dist/{errors-CW1LnxaQ.mjs → errors-BFUtdZfC.mjs} +0 -0
  51. /package/dist/{utils-D-xTRNuh.mjs → utils-DBCCkkJS.mjs} +0 -0
package/dist/mcp.mjs CHANGED
@@ -1,6 +1,7 @@
1
- import { a as emitDocsAnalyticsEvent, i as emitDocsAgentTraceEvent, n as createDocsAgentTraceContext, r as createDocsAgentTraceId } from "./analytics-BZEwTK-8.mjs";
2
- import { Nt as normalizeDocsRelated, Pt as renderDocsRelatedMarkdownLines } from "./agent-CaOBIVFy.mjs";
3
- import { D as emitDocsTelemetryProjectEvent, E as emitDocsTelemetryMcpToolEvent, u as performDocsSearch, w as emitDocsTelemetryAgentSurfaceEvent, x as resolvePageSidebarFolderIndexBehavior, y as stripGeneratedAgentProvenance } from "./search-BWuU70B0.mjs";
1
+ import { a as emitDocsAnalyticsEvent, i as emitDocsAgentTraceEvent, n as createDocsAgentTraceContext, r as createDocsAgentTraceId } from "./analytics-Bx44lg6d.mjs";
2
+ import { Gt as hasStructuredPageAgentContract, Kt as normalizePageAgentFrontmatter, Lt as normalizeDocsRelated, Rt as renderDocsRelatedMarkdownLines, Xt as upsertPageAgentContractMarkdown } from "./agent-DXkXi-pS.mjs";
3
+ import { C as resolvePageSidebarFolderIndexBehavior, E as emitDocsTelemetryAgentSurfaceEvent, O as emitDocsTelemetryMcpToolEvent, k as emitDocsTelemetryProjectEvent, m as parseDocsMarkdownSections, p as findDocsMarkdownSection, u as performDocsSearch, x as stripGeneratedAgentProvenance } from "./search-CJIC1Cqo.mjs";
4
+ import { a as normalizeAgentScopeValues, i as normalizeAgentLocale, n as agentVersionConstraintsOverlap, r as normalizeAgentFramework, t as agentVersionConstraintMatches } from "./agent-scope-B8lptqCd.mjs";
4
5
  import matter from "gray-matter";
5
6
  import fs from "node:fs";
6
7
  import path from "node:path";
@@ -14,7 +15,41 @@ import * as z from "zod/v4";
14
15
  const DEFAULT_MCP_ROUTE = "/api/docs/mcp";
15
16
  const DEFAULT_MCP_VERSION = "0.0.0";
16
17
  const DEFAULT_MCP_NAME = "@farming-labs/docs";
17
- const DOCS_CONFIG_SCHEMA_OPTIONS = [
18
+ const DEFAULT_MCP_CONTEXT_TOKEN_BUDGET = 4e3;
19
+ const MIN_MCP_CONTEXT_TOKEN_BUDGET = 256;
20
+ const MAX_MCP_CONTEXT_TOKEN_BUDGET = 32e3;
21
+ const UTF8_ENCODER = new TextEncoder();
22
+ const DEFAULT_DOCS_MCP_MAX_BODY_BYTES = 1024 * 1024;
23
+ const DEFAULT_DOCS_MCP_CORS_MAX_AGE_SECONDS = 600;
24
+ const DEFAULT_DOCS_MCP_CORS_ALLOWED_HEADERS = Object.freeze([
25
+ "Accept",
26
+ "Authorization",
27
+ "Content-Type",
28
+ "Last-Event-ID",
29
+ "MCP-Protocol-Version",
30
+ "MCP-Session-Id"
31
+ ]);
32
+ const DEFAULT_DOCS_MCP_CORS_EXPOSED_HEADERS = Object.freeze([
33
+ "MCP-Protocol-Version",
34
+ "MCP-Session-Id",
35
+ "WWW-Authenticate"
36
+ ]);
37
+ function freezeDocsConfigSchemaOptions(options) {
38
+ for (const option of options) {
39
+ if (option.values) Object.freeze(option.values);
40
+ if (option.children) {
41
+ freezeDocsConfigSchemaOptions([...option.children]);
42
+ Object.freeze(option.children);
43
+ }
44
+ Object.freeze(option);
45
+ }
46
+ return Object.freeze(options);
47
+ }
48
+ function freezeDocsConfigSchemaExamples(examples) {
49
+ for (const example of examples) Object.freeze(example);
50
+ return Object.freeze(examples);
51
+ }
52
+ const DOCS_CONFIG_SCHEMA_OPTIONS_TEMPLATE = [
18
53
  {
19
54
  path: "entry",
20
55
  name: "entry",
@@ -23,6 +58,14 @@ const DOCS_CONFIG_SCHEMA_OPTIONS = [
23
58
  description: "URL path prefix for documentation routes, for example \"docs\" creates /docs.",
24
59
  docs: "/docs/overview"
25
60
  },
61
+ {
62
+ path: "docsPath",
63
+ name: "docsPath",
64
+ type: "string",
65
+ default: "same as entry",
66
+ description: "Public route prefix for docs pages when it differs from the source entry directory.",
67
+ docs: "/docs/overview"
68
+ },
26
69
  {
27
70
  path: "contentDir",
28
71
  name: "contentDir",
@@ -31,6 +74,13 @@ const DOCS_CONFIG_SCHEMA_OPTIONS = [
31
74
  description: "Path to markdown content files. Adapters outside Next.js usually need this when content does not live under the route prefix.",
32
75
  docs: "/docs/overview"
33
76
  },
77
+ {
78
+ path: "i18n",
79
+ name: "i18n",
80
+ type: "DocsI18nConfig",
81
+ description: "Locale discovery, default locale, and localized docs content configuration.",
82
+ docs: "/docs/reference"
83
+ },
34
84
  {
35
85
  path: "staticExport",
36
86
  name: "staticExport",
@@ -46,6 +96,25 @@ const DOCS_CONFIG_SCHEMA_OPTIONS = [
46
96
  description: "Theme instance from a theme factory such as fumadocs() or pixelBorder().",
47
97
  docs: "/docs/customization/themes"
48
98
  },
99
+ {
100
+ path: "analytics",
101
+ name: "analytics",
102
+ type: "boolean | DocsAnalyticsConfig",
103
+ default: false,
104
+ description: "Built-in privacy-aware product and agent surface analytics."
105
+ },
106
+ {
107
+ path: "telemetry",
108
+ name: "telemetry",
109
+ type: "boolean | DocsTelemetryConfig",
110
+ description: "Project telemetry controls for framework and agent-surface events."
111
+ },
112
+ {
113
+ path: "observability",
114
+ name: "observability",
115
+ type: "boolean | DocsObservabilityConfig",
116
+ description: "Tracing and observability callbacks for search, AI, and agent operations."
117
+ },
49
118
  {
50
119
  path: "nav",
51
120
  name: "nav",
@@ -135,12 +204,419 @@ const DOCS_CONFIG_SCHEMA_OPTIONS = [
135
204
  default: false,
136
205
  description: "Opt-in estimated reading time label with per-page overrides and label format."
137
206
  },
207
+ {
208
+ path: "lastUpdated",
209
+ name: "lastUpdated",
210
+ type: "boolean | LastUpdatedConfig",
211
+ description: "Last-updated metadata and labels derived from source history or page data."
212
+ },
213
+ {
214
+ path: "ordering",
215
+ name: "ordering",
216
+ type: "\"alphabetical\" | \"numeric\" | OrderingItem[]",
217
+ description: "Navigation ordering strategy or explicit ordered navigation entries."
218
+ },
138
219
  {
139
220
  path: "agent",
140
221
  name: "agent",
141
222
  type: "DocsAgentConfig",
142
- description: "Defaults for docs agent compact and generated agent-facing files.",
143
- docs: "/docs/getting-started/agent-ready-docs"
223
+ description: "Agent compaction defaults and deterministic usefulness evaluations.",
224
+ docs: "/docs/getting-started/agent-ready-docs",
225
+ children: [{
226
+ path: "agent.compact",
227
+ name: "compact",
228
+ type: "DocsAgentCompactConfig",
229
+ description: "Defaults for generated agent.md compaction.",
230
+ children: [
231
+ {
232
+ path: "agent.compact.apiKey",
233
+ name: "apiKey",
234
+ type: "string",
235
+ description: "Direct compaction provider API key; prefer apiKeyEnv."
236
+ },
237
+ {
238
+ path: "agent.compact.apiKeyEnv",
239
+ name: "apiKeyEnv",
240
+ type: "string",
241
+ description: "Environment variable containing the compaction provider API key."
242
+ },
243
+ {
244
+ path: "agent.compact.baseUrl",
245
+ name: "baseUrl",
246
+ type: "string",
247
+ description: "Compaction provider base URL."
248
+ },
249
+ {
250
+ path: "agent.compact.model",
251
+ name: "model",
252
+ type: "string",
253
+ description: "Compaction model identifier."
254
+ },
255
+ {
256
+ path: "agent.compact.aggressiveness",
257
+ name: "aggressiveness",
258
+ type: "number",
259
+ default: .3,
260
+ description: "Compression aggressiveness from 0 to 1."
261
+ },
262
+ {
263
+ path: "agent.compact.maxOutputTokens",
264
+ name: "maxOutputTokens",
265
+ type: "number",
266
+ description: "Upper output token target."
267
+ },
268
+ {
269
+ path: "agent.compact.minOutputTokens",
270
+ name: "minOutputTokens",
271
+ type: "number",
272
+ description: "Lower output token target."
273
+ },
274
+ {
275
+ path: "agent.compact.protectJson",
276
+ name: "protectJson",
277
+ type: "boolean",
278
+ description: "Preserve JSON objects during compaction when supported."
279
+ }
280
+ ]
281
+ }, {
282
+ path: "agent.evaluations",
283
+ name: "evaluations",
284
+ type: "boolean | DocsAgentEvaluationsConfig",
285
+ description: "Deterministic golden tasks for retrieval, citation, version, example, and budget evaluation.",
286
+ children: [
287
+ {
288
+ path: "agent.evaluations.enabled",
289
+ name: "enabled",
290
+ type: "boolean",
291
+ default: true,
292
+ description: "Enable configured golden-task evaluation."
293
+ },
294
+ {
295
+ path: "agent.evaluations.tokenBudget",
296
+ name: "tokenBudget",
297
+ type: "number",
298
+ default: 4e3,
299
+ description: "Default hard UTF-8 context-byte ceiling for golden tasks."
300
+ },
301
+ {
302
+ path: "agent.evaluations.topK",
303
+ name: "topK",
304
+ type: "number",
305
+ default: 5,
306
+ description: "Default number of ranked search results evaluated per task."
307
+ },
308
+ {
309
+ path: "agent.evaluations.tasks",
310
+ name: "tasks",
311
+ type: "DocsAgentGoldenTask[]",
312
+ description: "Offline golden task fixtures evaluated by docs doctor and docs review.",
313
+ children: [
314
+ {
315
+ path: "agent.evaluations.tasks[]",
316
+ name: "task",
317
+ type: "DocsAgentGoldenTask",
318
+ description: "One golden task entry."
319
+ },
320
+ {
321
+ path: "agent.evaluations.tasks[].id",
322
+ name: "id",
323
+ type: "string",
324
+ description: "Stable task identifier shown in diagnostics."
325
+ },
326
+ {
327
+ path: "agent.evaluations.tasks[].query",
328
+ name: "query",
329
+ type: "string",
330
+ description: "User-shaped retrieval query."
331
+ },
332
+ {
333
+ path: "agent.evaluations.tasks[].tokenBudget",
334
+ name: "tokenBudget",
335
+ type: "number",
336
+ description: "Per-task UTF-8 context-byte ceiling override."
337
+ },
338
+ {
339
+ path: "agent.evaluations.tasks[].topK",
340
+ name: "topK",
341
+ type: "number",
342
+ description: "Per-task ranked retrieval depth override."
343
+ },
344
+ {
345
+ path: "agent.evaluations.tasks[].filters",
346
+ name: "filters",
347
+ type: "DocsAgentGoldenTaskFilters",
348
+ description: "Framework, version, and locale retrieval scope.",
349
+ children: [
350
+ {
351
+ path: "agent.evaluations.tasks[].filters.framework",
352
+ name: "framework",
353
+ type: "string",
354
+ description: "Required framework, such as nextjs or astro."
355
+ },
356
+ {
357
+ path: "agent.evaluations.tasks[].filters.version",
358
+ name: "version",
359
+ type: "string",
360
+ description: "Exact version requested by the task."
361
+ },
362
+ {
363
+ path: "agent.evaluations.tasks[].filters.locale",
364
+ name: "locale",
365
+ type: "string",
366
+ description: "Required locale."
367
+ }
368
+ ]
369
+ },
370
+ {
371
+ path: "agent.evaluations.tasks[].expect",
372
+ name: "expect",
373
+ type: "DocsAgentGoldenTaskExpectation",
374
+ description: "Deterministic sources, rank, citation, example, and budget expectations.",
375
+ children: [
376
+ {
377
+ path: "agent.evaluations.tasks[].expect.relevantSources",
378
+ name: "relevantSources",
379
+ type: "string[]",
380
+ description: "Canonical page or section URLs that should answer the task."
381
+ },
382
+ {
383
+ path: "agent.evaluations.tasks[].expect.allowedSources",
384
+ name: "allowedSources",
385
+ type: "string[]",
386
+ description: "Additional legitimate citations that do not reduce precision."
387
+ },
388
+ {
389
+ path: "agent.evaluations.tasks[].expect.forbiddenSources",
390
+ name: "forbiddenSources",
391
+ type: "string[]",
392
+ description: "Sources that must not be retrieved or cited."
393
+ },
394
+ {
395
+ path: "agent.evaluations.tasks[].expect.requiredCitations",
396
+ name: "requiredCitations",
397
+ type: "string[]",
398
+ description: "Citations that must appear; defaults to relevantSources."
399
+ },
400
+ {
401
+ path: "agent.evaluations.tasks[].expect.minRecallAtK",
402
+ name: "minRecallAtK",
403
+ type: "number",
404
+ default: 1,
405
+ description: "Minimum relevant-source recall in the top K results."
406
+ },
407
+ {
408
+ path: "agent.evaluations.tasks[].expect.maxFirstRelevantRank",
409
+ name: "maxFirstRelevantRank",
410
+ type: "number",
411
+ description: "Maximum acceptable rank of the first relevant source."
412
+ },
413
+ {
414
+ path: "agent.evaluations.tasks[].expect.minUsefulByteRatio",
415
+ name: "minUsefulByteRatio",
416
+ type: "number",
417
+ description: "Minimum share of context bytes supplied by relevant sources."
418
+ },
419
+ {
420
+ path: "agent.evaluations.tasks[].expect.examples",
421
+ name: "examples",
422
+ type: "DocsAgentGoldenExpectedExample[]",
423
+ description: "Runnable examples that must be present in returned context.",
424
+ children: [
425
+ {
426
+ path: "agent.evaluations.tasks[].expect.examples[]",
427
+ name: "example",
428
+ type: "DocsAgentGoldenExpectedExample",
429
+ description: "One expected code example."
430
+ },
431
+ {
432
+ path: "agent.evaluations.tasks[].expect.examples[].source",
433
+ name: "source",
434
+ type: "string",
435
+ description: "Canonical source URL containing the example."
436
+ },
437
+ {
438
+ path: "agent.evaluations.tasks[].expect.examples[].language",
439
+ name: "language",
440
+ type: "string",
441
+ description: "Expected code-fence language."
442
+ },
443
+ {
444
+ path: "agent.evaluations.tasks[].expect.examples[].framework",
445
+ name: "framework",
446
+ type: "string",
447
+ description: "Expected framework metadata."
448
+ },
449
+ {
450
+ path: "agent.evaluations.tasks[].expect.examples[].packageManager",
451
+ name: "packageManager",
452
+ type: "string",
453
+ description: "Expected package-manager metadata."
454
+ },
455
+ {
456
+ path: "agent.evaluations.tasks[].expect.examples[].title",
457
+ name: "title",
458
+ type: "string",
459
+ description: "Expected code-fence title metadata."
460
+ },
461
+ {
462
+ path: "agent.evaluations.tasks[].expect.examples[].runnable",
463
+ name: "runnable",
464
+ type: "boolean",
465
+ default: true,
466
+ description: "Whether the example must be marked runnable."
467
+ },
468
+ {
469
+ path: "agent.evaluations.tasks[].expect.examples[].includes",
470
+ name: "includes",
471
+ type: "string[]",
472
+ description: "Literal code fragments that must appear."
473
+ }
474
+ ]
475
+ }
476
+ ]
477
+ }
478
+ ]
479
+ }
480
+ ]
481
+ }]
482
+ },
483
+ {
484
+ path: "review",
485
+ name: "review",
486
+ type: "boolean | DocsReviewConfig",
487
+ description: "Docs review scoring, CI behavior, and diagnostic rule severities.",
488
+ docs: "/docs/reference",
489
+ children: [
490
+ {
491
+ path: "review.enabled",
492
+ name: "enabled",
493
+ type: "boolean",
494
+ default: true,
495
+ description: "Enable Docs Review."
496
+ },
497
+ {
498
+ path: "review.score",
499
+ name: "score",
500
+ type: "DocsReviewScoreConfig",
501
+ description: "Healthy threshold and finding severity weights.",
502
+ children: [{
503
+ path: "review.score.threshold",
504
+ name: "threshold",
505
+ type: "number",
506
+ default: 80,
507
+ description: "Minimum healthy review score."
508
+ }, {
509
+ path: "review.score.weights",
510
+ name: "weights",
511
+ type: "{ error?: number; warn?: number; suggestion?: number }",
512
+ description: "Point deductions for each finding severity.",
513
+ children: [
514
+ {
515
+ path: "review.score.weights.error",
516
+ name: "error",
517
+ type: "number",
518
+ default: 20,
519
+ description: "Point deduction for an error finding."
520
+ },
521
+ {
522
+ path: "review.score.weights.warn",
523
+ name: "warn",
524
+ type: "number",
525
+ default: 8,
526
+ description: "Point deduction for a warning finding."
527
+ },
528
+ {
529
+ path: "review.score.weights.suggestion",
530
+ name: "suggestion",
531
+ type: "number",
532
+ default: 2,
533
+ description: "Point deduction for a suggestion finding."
534
+ }
535
+ ]
536
+ }]
537
+ },
538
+ {
539
+ path: "review.ci",
540
+ name: "ci",
541
+ type: "boolean | DocsReviewCiConfig",
542
+ description: "GitHub Actions reporting and blocking behavior.",
543
+ children: [
544
+ {
545
+ path: "review.ci.enabled",
546
+ name: "enabled",
547
+ type: "boolean",
548
+ default: true,
549
+ description: "Enable review workflow generation."
550
+ },
551
+ {
552
+ path: "review.ci.name",
553
+ name: "name",
554
+ type: "string",
555
+ default: "docs-review",
556
+ description: "GitHub Actions job and check name."
557
+ },
558
+ {
559
+ path: "review.ci.mode",
560
+ name: "mode",
561
+ type: "\"off\" | \"warn\" | \"block\"",
562
+ default: "warn",
563
+ description: "Whether CI is disabled, advisory, or blocking.",
564
+ values: [
565
+ "off",
566
+ "warn",
567
+ "block"
568
+ ]
569
+ },
570
+ {
571
+ path: "review.ci.annotations",
572
+ name: "annotations",
573
+ type: "boolean",
574
+ default: true,
575
+ description: "Emit GitHub workflow annotations."
576
+ },
577
+ {
578
+ path: "review.ci.comment",
579
+ name: "comment",
580
+ type: "boolean",
581
+ default: true,
582
+ description: "Allow the official action or bot to post PR comments."
583
+ }
584
+ ]
585
+ },
586
+ {
587
+ path: "review.rules",
588
+ name: "rules",
589
+ type: "DocsReviewRulesConfig",
590
+ description: "Per-rule severity overrides.",
591
+ children: [
592
+ ["brokenLinks", "error"],
593
+ ["frontmatter", "error"],
594
+ ["duplicateSlugs", "error"],
595
+ ["invalidMdx", "error"],
596
+ ["configExamples", "warn"],
597
+ ["codeFenceMetadata", "warn"],
598
+ ["runnableMetadata", "warn"],
599
+ ["agentContext", "warn"],
600
+ ["commandHealth", "warn"],
601
+ ["relatedCoverage", "suggestion"],
602
+ ["configConfidence", "warn"],
603
+ ["agentSurfaceDrift", "error"],
604
+ ["goldenTasks", "warn"]
605
+ ].map(([name, defaultValue]) => ({
606
+ path: `review.rules.${name}`,
607
+ name,
608
+ type: "DocsReviewSeverity",
609
+ default: defaultValue,
610
+ description: `Severity override for the ${name} review rule.`,
611
+ values: [
612
+ "off",
613
+ "suggestion",
614
+ "warn",
615
+ "error"
616
+ ]
617
+ }))
618
+ }
619
+ ]
144
620
  },
145
621
  {
146
622
  path: "pageActions",
@@ -328,6 +804,70 @@ const DOCS_CONFIG_SCHEMA_OPTIONS = [
328
804
  default: "0.0.0",
329
805
  description: "Version string reported to MCP clients."
330
806
  },
807
+ {
808
+ path: "mcp.security",
809
+ name: "security",
810
+ type: "DocsMcpSecurityConfig",
811
+ description: "Streamable HTTP Origin validation, optional authentication, and request-size controls. The stdio transport is unaffected.",
812
+ children: [
813
+ {
814
+ path: "mcp.security.allowedOrigins",
815
+ name: "allowedOrigins",
816
+ type: "\"same-origin\" | string[] | callback",
817
+ default: "same-origin",
818
+ description: "Allow a supplied Origin header when it matches the MCP request origin, an explicit list, or a custom policy callback. Origin-less non-browser clients remain supported."
819
+ },
820
+ {
821
+ path: "mcp.security.authenticate",
822
+ name: "authenticate",
823
+ type: "DocsMcpAuthenticate",
824
+ default: "public (callback omitted)",
825
+ description: "Opt-in HTTP authentication callback. Return a principal to continue, null for 401, or a Response to control the rejection."
826
+ },
827
+ {
828
+ path: "mcp.security.maxBodyBytes",
829
+ name: "maxBodyBytes",
830
+ type: "number",
831
+ default: DEFAULT_DOCS_MCP_MAX_BODY_BYTES,
832
+ description: "Maximum accepted Streamable HTTP POST body size in bytes."
833
+ },
834
+ {
835
+ path: "mcp.security.cors",
836
+ name: "cors",
837
+ type: "boolean | DocsMcpCorsConfig",
838
+ default: true,
839
+ description: "Emit exact-Origin CORS responses for Origins accepted by allowedOrigins. Use an object for credentials, additional headers, and preflight cache controls.",
840
+ children: [
841
+ {
842
+ path: "mcp.security.cors.allowedHeaders",
843
+ name: "allowedHeaders",
844
+ type: "string[]",
845
+ description: "Additional request headers accepted during browser preflight."
846
+ },
847
+ {
848
+ path: "mcp.security.cors.exposedHeaders",
849
+ name: "exposedHeaders",
850
+ type: "string[]",
851
+ description: "Additional MCP response headers exposed to browser JavaScript."
852
+ },
853
+ {
854
+ path: "mcp.security.cors.allowCredentials",
855
+ name: "allowCredentials",
856
+ type: "boolean",
857
+ default: false,
858
+ description: "Allow credentialed browser requests using the validated exact Origin. Wildcard credentials are never emitted."
859
+ },
860
+ {
861
+ path: "mcp.security.cors.maxAgeSeconds",
862
+ name: "maxAgeSeconds",
863
+ type: "number",
864
+ default: DEFAULT_DOCS_MCP_CORS_MAX_AGE_SECONDS,
865
+ description: "Browser preflight cache lifetime in seconds."
866
+ }
867
+ ]
868
+ }
869
+ ]
870
+ },
331
871
  {
332
872
  path: "mcp.tools",
333
873
  name: "tools",
@@ -349,6 +889,20 @@ const DOCS_CONFIG_SCHEMA_OPTIONS = [
349
889
  default: true,
350
890
  description: "Expose the list_pages tool."
351
891
  },
892
+ {
893
+ path: "mcp.tools.listTasks",
894
+ name: "listTasks",
895
+ type: "boolean",
896
+ default: true,
897
+ description: "Expose the list_tasks tool."
898
+ },
899
+ {
900
+ path: "mcp.tools.readTask",
901
+ name: "readTask",
902
+ type: "boolean",
903
+ default: true,
904
+ description: "Expose the read_task tool."
905
+ },
352
906
  {
353
907
  path: "mcp.tools.getNavigation",
354
908
  name: "getNavigation",
@@ -383,6 +937,13 @@ const DOCS_CONFIG_SCHEMA_OPTIONS = [
383
937
  type: "boolean",
384
938
  default: true,
385
939
  description: "Expose the get_config_schema tool."
940
+ },
941
+ {
942
+ path: "mcp.tools.getContext",
943
+ name: "getContext",
944
+ type: "boolean",
945
+ default: true,
946
+ description: "Expose deterministic get_context retrieval with a conservative UTF-8 byte ceiling."
386
947
  }
387
948
  ]
388
949
  }
@@ -470,7 +1031,8 @@ const DOCS_CONFIG_SCHEMA_OPTIONS = [
470
1031
  description: "Dynamic Open Graph image configuration."
471
1032
  }
472
1033
  ];
473
- const DOCS_CONFIG_SCHEMA_EXAMPLES = [
1034
+ const DOCS_CONFIG_SCHEMA_OPTIONS = freezeDocsConfigSchemaOptions(DOCS_CONFIG_SCHEMA_OPTIONS_TEMPLATE);
1035
+ const DOCS_CONFIG_SCHEMA_EXAMPLES = freezeDocsConfigSchemaExamples([
474
1036
  {
475
1037
  title: "Minimal config",
476
1038
  code: `import { defineDocs } from "@farming-labs/docs";
@@ -492,6 +1054,19 @@ export default defineDocs({
492
1054
  getCodeExamples: true,
493
1055
  },
494
1056
  },
1057
+ });`
1058
+ },
1059
+ {
1060
+ title: "Opt-in MCP authentication",
1061
+ code: `export default defineDocs({
1062
+ mcp: {
1063
+ security: {
1064
+ async authenticate({ request }) {
1065
+ const user = await authenticateRequest(request);
1066
+ return user ? { id: user.id, scopes: ["docs:read"] } : null;
1067
+ },
1068
+ },
1069
+ },
495
1070
  });`
496
1071
  },
497
1072
  {
@@ -516,7 +1091,7 @@ export default defineDocs({
516
1091
  },
517
1092
  });`
518
1093
  }
519
- ];
1094
+ ]);
520
1095
  const searchDocsInputSchema = z.object({
521
1096
  query: z.string().trim().min(1),
522
1097
  limit: z.number().int().min(1).max(25).optional(),
@@ -524,8 +1099,74 @@ const searchDocsInputSchema = z.object({
524
1099
  });
525
1100
  const readPageInputSchema = z.object({
526
1101
  path: z.string().min(1),
1102
+ locale: z.string().min(1).optional(),
1103
+ section: z.string().trim().min(1).optional(),
1104
+ maxChars: z.number().int().min(256).max(1e6).optional()
1105
+ });
1106
+ const listTasksInputSchema = z.object({
1107
+ query: z.string().trim().min(1).optional(),
1108
+ framework: z.string().trim().min(1).optional(),
1109
+ version: z.string().trim().min(1).optional(),
1110
+ package: z.string().trim().min(1).optional(),
527
1111
  locale: z.string().min(1).optional()
528
1112
  });
1113
+ const readTaskInputSchema = readPageInputSchema;
1114
+ const pageAgentAppliesToOutputSchema = z.object({
1115
+ framework: z.array(z.string()).optional(),
1116
+ version: z.array(z.string()).optional(),
1117
+ package: z.array(z.string()).optional()
1118
+ });
1119
+ const pageAgentCommandOutputSchema = z.union([z.string(), z.object({
1120
+ run: z.string(),
1121
+ cwd: z.string().optional(),
1122
+ description: z.string().optional()
1123
+ })]);
1124
+ const pageAgentVerificationOutputSchema = z.union([z.string(), z.object({
1125
+ description: z.string().optional(),
1126
+ run: z.string().optional(),
1127
+ expect: z.string().optional()
1128
+ })]);
1129
+ const pageAgentFailureModeOutputSchema = z.union([z.string(), z.object({
1130
+ symptom: z.string(),
1131
+ resolution: z.string().optional()
1132
+ })]);
1133
+ const pageAgentContractOutputSchema = z.object({
1134
+ tokenBudget: z.number().optional(),
1135
+ task: z.string().optional(),
1136
+ outcome: z.string().optional(),
1137
+ appliesTo: pageAgentAppliesToOutputSchema.optional(),
1138
+ prerequisites: z.array(z.string()).optional(),
1139
+ files: z.array(z.string()).optional(),
1140
+ commands: z.array(pageAgentCommandOutputSchema).optional(),
1141
+ sideEffects: z.array(z.string()).optional(),
1142
+ verification: z.array(pageAgentVerificationOutputSchema).optional(),
1143
+ rollback: z.array(z.string()).optional(),
1144
+ failureModes: z.array(pageAgentFailureModeOutputSchema).optional()
1145
+ });
1146
+ const taskSummaryOutputSchema = z.object({
1147
+ slug: z.string(),
1148
+ url: z.string(),
1149
+ title: z.string(),
1150
+ description: z.string().optional(),
1151
+ task: z.string().optional(),
1152
+ outcome: z.string().optional(),
1153
+ appliesTo: pageAgentAppliesToOutputSchema.optional()
1154
+ });
1155
+ const listTasksOutputSchema = z.object({
1156
+ resultCount: z.number().int().nonnegative(),
1157
+ tasks: z.array(taskSummaryOutputSchema)
1158
+ });
1159
+ const readTaskOutputSchema = z.object({
1160
+ page: z.object({
1161
+ slug: z.string(),
1162
+ url: z.string(),
1163
+ title: z.string(),
1164
+ description: z.string().optional(),
1165
+ sourcePath: z.string().optional(),
1166
+ lastModified: z.string().optional()
1167
+ }),
1168
+ contract: pageAgentContractOutputSchema
1169
+ });
529
1170
  const listPagesInputSchema = z.object({ locale: z.string().min(1).optional() });
530
1171
  const listDocsInputSchema = z.object({
531
1172
  section: z.string().trim().min(1).optional(),
@@ -546,6 +1187,195 @@ const getCodeExamplesInputSchema = z.object({
546
1187
  limit: z.number().int().min(1).max(50).optional(),
547
1188
  locale: z.string().min(1).optional()
548
1189
  });
1190
+ const getContextInputSchema = z.object({
1191
+ query: z.string().trim().min(1),
1192
+ framework: z.string().trim().min(1).optional(),
1193
+ version: z.string().trim().min(1).optional(),
1194
+ locale: z.string().trim().min(1).optional(),
1195
+ tokenBudget: z.number().int().min(MIN_MCP_CONTEXT_TOKEN_BUDGET).max(MAX_MCP_CONTEXT_TOKEN_BUDGET).default(DEFAULT_MCP_CONTEXT_TOKEN_BUDGET)
1196
+ });
1197
+ const relatedLinkOutputSchema = z.object({ href: z.string() });
1198
+ const pageAgentContractSummaryOutputSchema = z.object({
1199
+ hasContract: z.boolean(),
1200
+ task: z.string().optional(),
1201
+ outcome: z.string().optional(),
1202
+ appliesTo: pageAgentAppliesToOutputSchema.optional()
1203
+ });
1204
+ const pageSummaryOutputSchema = z.object({
1205
+ slug: z.string(),
1206
+ url: z.string(),
1207
+ title: z.string(),
1208
+ description: z.string().optional(),
1209
+ agent: pageAgentContractSummaryOutputSchema.optional(),
1210
+ icon: z.string().optional(),
1211
+ sourcePath: z.string().optional(),
1212
+ lastModified: z.string().optional()
1213
+ });
1214
+ const docsSectionOutputSchema = z.lazy(() => z.object({
1215
+ slug: z.string(),
1216
+ title: z.string(),
1217
+ url: z.string().optional(),
1218
+ description: z.string().optional(),
1219
+ icon: z.string().optional(),
1220
+ pageCount: z.number().int().nonnegative(),
1221
+ pages: z.array(pageSummaryOutputSchema),
1222
+ sections: z.array(docsSectionOutputSchema)
1223
+ }));
1224
+ const listPagesOutputSchema = z.object({ pages: z.array(pageSummaryOutputSchema) });
1225
+ const listDocsOutputSchema = z.object({
1226
+ section: z.string().optional(),
1227
+ resultCount: z.number().int().nonnegative(),
1228
+ sectionCount: z.number().int().nonnegative(),
1229
+ pages: z.array(pageSummaryOutputSchema),
1230
+ rootPages: z.array(pageSummaryOutputSchema),
1231
+ sections: z.array(docsSectionOutputSchema)
1232
+ });
1233
+ const navigationPageOutputSchema = z.object({
1234
+ type: z.literal("page"),
1235
+ name: z.string(),
1236
+ url: z.string(),
1237
+ icon: z.string().optional(),
1238
+ description: z.string().optional()
1239
+ });
1240
+ const navigationNodeOutputSchema = z.lazy(() => z.discriminatedUnion("type", [navigationPageOutputSchema, z.object({
1241
+ type: z.literal("folder"),
1242
+ name: z.string(),
1243
+ icon: z.string().optional(),
1244
+ index: navigationPageOutputSchema.optional(),
1245
+ children: z.array(navigationNodeOutputSchema)
1246
+ })]));
1247
+ const navigationOutputSchema = z.object({
1248
+ navigation: z.object({
1249
+ name: z.string(),
1250
+ children: z.array(navigationNodeOutputSchema)
1251
+ }),
1252
+ markdown: z.string()
1253
+ });
1254
+ const searchResultOutputSchema = z.object({
1255
+ id: z.string(),
1256
+ url: z.string(),
1257
+ content: z.string(),
1258
+ description: z.string().optional(),
1259
+ type: z.enum([
1260
+ "page",
1261
+ "heading",
1262
+ "text"
1263
+ ]),
1264
+ score: z.number().optional(),
1265
+ section: z.string().optional()
1266
+ });
1267
+ const searchDocsOutputSchema = z.object({ results: z.array(searchResultOutputSchema) });
1268
+ const codeExampleOutputSchema = z.object({
1269
+ id: z.string(),
1270
+ page: z.object({
1271
+ slug: z.string(),
1272
+ url: z.string(),
1273
+ title: z.string(),
1274
+ description: z.string().optional(),
1275
+ sourcePath: z.string().optional(),
1276
+ lastModified: z.string().optional()
1277
+ }),
1278
+ language: z.string().optional(),
1279
+ title: z.string().optional(),
1280
+ framework: z.string().optional(),
1281
+ packageManager: z.string().optional(),
1282
+ runnable: z.boolean(),
1283
+ meta: z.record(z.string(), z.union([z.string(), z.boolean()])),
1284
+ code: z.string()
1285
+ });
1286
+ const codeExamplesOutputSchema = z.object({ examples: z.array(codeExampleOutputSchema) });
1287
+ const configSchemaOptionOutputSchema = z.lazy(() => z.object({
1288
+ path: z.string(),
1289
+ name: z.string(),
1290
+ type: z.string(),
1291
+ default: z.union([
1292
+ z.string(),
1293
+ z.boolean(),
1294
+ z.number(),
1295
+ z.null()
1296
+ ]).optional(),
1297
+ description: z.string(),
1298
+ docs: z.string().optional(),
1299
+ values: z.array(z.string()).optional(),
1300
+ children: z.array(configSchemaOptionOutputSchema).optional()
1301
+ }));
1302
+ const configSchemaOutputSchema = z.object({
1303
+ schemaVersion: z.literal(1),
1304
+ configFile: z.literal("docs.config.ts"),
1305
+ description: z.string(),
1306
+ filters: z.object({
1307
+ option: z.string().optional(),
1308
+ query: z.string().optional()
1309
+ }).optional(),
1310
+ resultCount: z.number().int().nonnegative(),
1311
+ options: z.array(configSchemaOptionOutputSchema),
1312
+ examples: z.array(z.object({
1313
+ title: z.string(),
1314
+ code: z.string()
1315
+ }))
1316
+ });
1317
+ const readPageOutputSchema = z.object({
1318
+ page: z.object({
1319
+ slug: z.string(),
1320
+ url: z.string(),
1321
+ title: z.string(),
1322
+ description: z.string().optional(),
1323
+ related: z.array(relatedLinkOutputSchema).optional(),
1324
+ icon: z.string().optional(),
1325
+ sourcePath: z.string().optional(),
1326
+ lastModified: z.string().optional(),
1327
+ locale: z.string().optional(),
1328
+ framework: z.string().optional(),
1329
+ version: z.string().optional(),
1330
+ tags: z.array(z.string()).optional()
1331
+ }),
1332
+ document: z.string(),
1333
+ section: z.string().optional(),
1334
+ anchor: z.string().optional(),
1335
+ chars: z.number().int().nonnegative(),
1336
+ totalChars: z.number().int().nonnegative(),
1337
+ truncated: z.boolean()
1338
+ });
1339
+ const contextSourceOutputSchema = z.object({
1340
+ id: z.string(),
1341
+ title: z.string(),
1342
+ pageUrl: z.string(),
1343
+ url: z.string(),
1344
+ section: z.string().optional(),
1345
+ anchor: z.string().optional(),
1346
+ sourcePath: z.string().optional(),
1347
+ lastModified: z.string().optional(),
1348
+ locale: z.string().optional(),
1349
+ framework: z.string().optional(),
1350
+ version: z.string().optional(),
1351
+ tags: z.array(z.string()).optional(),
1352
+ score: z.number().optional(),
1353
+ content: z.string(),
1354
+ chars: z.number().int().nonnegative(),
1355
+ utf8Bytes: z.number().int().nonnegative(),
1356
+ truncated: z.boolean()
1357
+ });
1358
+ const contextOutputSchema = z.object({
1359
+ query: z.string(),
1360
+ filters: z.object({
1361
+ framework: z.string().optional(),
1362
+ version: z.string().optional(),
1363
+ locale: z.string().optional()
1364
+ }),
1365
+ budget: z.object({
1366
+ requestedTokens: z.number().int().positive(),
1367
+ strategy: z.literal("utf8-bytes"),
1368
+ maxUtf8Bytes: z.number().int().positive(),
1369
+ usedUtf8Bytes: z.number().int().nonnegative(),
1370
+ conservativeTokenUpperBound: z.number().int().nonnegative(),
1371
+ remainingUtf8Bytes: z.number().int().nonnegative(),
1372
+ truncated: z.boolean()
1373
+ }),
1374
+ resultCount: z.number().int().nonnegative(),
1375
+ candidateCount: z.number().int().nonnegative(),
1376
+ context: z.string(),
1377
+ sources: z.array(contextSourceOutputSchema)
1378
+ });
549
1379
  function normalizeDocsMcpRoute(route) {
550
1380
  if (!route || route.trim().length === 0) return DEFAULT_MCP_ROUTE;
551
1381
  const normalized = `/${route}`.replace(/\/+/g, "/");
@@ -561,11 +1391,15 @@ function resolveDocsMcpConfig(mcp, defaults = {}) {
561
1391
  listDocs: true,
562
1392
  listPages: true,
563
1393
  readPage: true,
1394
+ listTasks: true,
1395
+ readTask: true,
564
1396
  searchDocs: true,
565
1397
  getNavigation: true,
566
1398
  getCodeExamples: true,
567
- getConfigSchema: true
568
- }
1399
+ getConfigSchema: true,
1400
+ getContext: true
1401
+ },
1402
+ security: resolveDocsMcpSecurityConfig()
569
1403
  };
570
1404
  const config = mcp && typeof mcp === "object" ? mcp : {};
571
1405
  return {
@@ -577,13 +1411,49 @@ function resolveDocsMcpConfig(mcp, defaults = {}) {
577
1411
  listDocs: config.tools?.listDocs ?? true,
578
1412
  listPages: config.tools?.listPages ?? true,
579
1413
  readPage: config.tools?.readPage ?? true,
1414
+ listTasks: config.tools?.listTasks ?? true,
1415
+ readTask: config.tools?.readTask ?? true,
580
1416
  searchDocs: config.tools?.searchDocs ?? true,
581
1417
  getNavigation: config.tools?.getNavigation ?? true,
582
1418
  getCodeExamples: config.tools?.getCodeExamples ?? true,
583
- getConfigSchema: config.tools?.getConfigSchema ?? true
584
- }
1419
+ getConfigSchema: config.tools?.getConfigSchema ?? true,
1420
+ getContext: config.tools?.getContext ?? true
1421
+ },
1422
+ security: resolveDocsMcpSecurityConfig(config.security)
1423
+ };
1424
+ }
1425
+ function resolveDocsMcpSecurityConfig(security) {
1426
+ const configuredMaxBodyBytes = security?.maxBodyBytes;
1427
+ const maxBodyBytes = typeof configuredMaxBodyBytes === "number" && Number.isFinite(configuredMaxBodyBytes) && configuredMaxBodyBytes > 0 ? Math.floor(configuredMaxBodyBytes) : DEFAULT_DOCS_MCP_MAX_BODY_BYTES;
1428
+ return {
1429
+ allowedOrigins: security?.allowedOrigins ?? "same-origin",
1430
+ authenticate: security?.authenticate,
1431
+ maxBodyBytes,
1432
+ cors: resolveDocsMcpCorsConfig(security?.cors)
585
1433
  };
586
1434
  }
1435
+ function resolveDocsMcpCorsConfig(cors) {
1436
+ const config = cors && typeof cors === "object" ? cors : {};
1437
+ const configuredMaxAge = config.maxAgeSeconds;
1438
+ const maxAgeSeconds = typeof configuredMaxAge === "number" && Number.isFinite(configuredMaxAge) && configuredMaxAge >= 0 ? Math.floor(configuredMaxAge) : DEFAULT_DOCS_MCP_CORS_MAX_AGE_SECONDS;
1439
+ return {
1440
+ enabled: cors !== false,
1441
+ allowedHeaders: mergeHttpHeaderNames(DEFAULT_DOCS_MCP_CORS_ALLOWED_HEADERS, config.allowedHeaders),
1442
+ exposedHeaders: mergeHttpHeaderNames(DEFAULT_DOCS_MCP_CORS_EXPOSED_HEADERS, config.exposedHeaders),
1443
+ allowCredentials: config.allowCredentials === true,
1444
+ maxAgeSeconds
1445
+ };
1446
+ }
1447
+ function mergeHttpHeaderNames(defaults, configured) {
1448
+ const headers = /* @__PURE__ */ new Map();
1449
+ for (const header of [...defaults, ...configured ?? []]) {
1450
+ const normalized = header.trim();
1451
+ if (!/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(normalized)) continue;
1452
+ const key = normalized.toLowerCase();
1453
+ if (!headers.has(key)) headers.set(key, normalized);
1454
+ }
1455
+ return [...headers.values()];
1456
+ }
587
1457
  function createFilesystemDocsMcpSource(options = {}) {
588
1458
  const rootDir = options.rootDir ?? process.cwd();
589
1459
  const entry = normalizePathSegment(options.entry ?? "docs") || "docs";
@@ -618,6 +1488,15 @@ function nowMs() {
618
1488
  function durationMs(startedAt) {
619
1489
  return Math.max(0, Date.now() - startedAt);
620
1490
  }
1491
+ function createStructuredTextResult(structuredContent, text) {
1492
+ return {
1493
+ content: [{
1494
+ type: "text",
1495
+ text: text ?? JSON.stringify(structuredContent, null, 2)
1496
+ }],
1497
+ structuredContent
1498
+ };
1499
+ }
621
1500
  async function createDocsMcpServer(options) {
622
1501
  const resolved = resolveDocsMcpConfig(options.mcp, {
623
1502
  defaultName: options.defaultName ?? options.source.siteTitle ?? DEFAULT_MCP_NAME,
@@ -634,6 +1513,12 @@ async function createDocsMcpServer(options) {
634
1513
  search: options.search
635
1514
  };
636
1515
  const telemetryFramework = options.telemetryFramework ?? "mcp";
1516
+ function getSourcePages(locale) {
1517
+ return options.source.getPages(locale, options.requestContext);
1518
+ }
1519
+ function getSourceNavigation(locale) {
1520
+ return options.source.getNavigation(locale, options.requestContext);
1521
+ }
637
1522
  function trackMcpTool(tool, values) {
638
1523
  emitDocsTelemetryMcpToolEvent(telemetryConfig, {
639
1524
  framework: telemetryFramework,
@@ -642,8 +1527,8 @@ async function createDocsMcpServer(options) {
642
1527
  resultCount: values?.resultCount
643
1528
  });
644
1529
  }
645
- const defaultPages = dedupePages(await options.source.getPages());
646
- const defaultTree = await options.source.getNavigation();
1530
+ const defaultPages = dedupePages(await getSourcePages());
1531
+ const defaultTree = await getSourceNavigation();
647
1532
  server.registerResource("docs-navigation", "docs://navigation", {
648
1533
  title: "Docs Navigation",
649
1534
  description: "Structured navigation tree for the documentation site.",
@@ -669,6 +1554,7 @@ async function createDocsMcpServer(options) {
669
1554
  title: "List docs pages",
670
1555
  description: "List the known documentation pages with titles, slugs, and URLs.",
671
1556
  inputSchema: listPagesInputSchema,
1557
+ outputSchema: listPagesOutputSchema,
672
1558
  annotations: { readOnlyHint: true }
673
1559
  }, async ({ locale }) => {
674
1560
  const startedAt = nowMs();
@@ -687,7 +1573,7 @@ async function createDocsMcpServer(options) {
687
1573
  metadata: { tool: "list_pages" }
688
1574
  });
689
1575
  try {
690
- const pages = toPageSummaries(dedupePages(await options.source.getPages(locale)));
1576
+ const pages = toPageSummaries(dedupePages(await getSourcePages(locale)));
691
1577
  const elapsed = durationMs(startedAt);
692
1578
  await emitDocsAnalyticsEvent(options.analytics, {
693
1579
  type: "mcp_tool",
@@ -717,10 +1603,7 @@ async function createDocsMcpServer(options) {
717
1603
  outputPreview: { resultCount: pages.length },
718
1604
  metadata: { tool: "list_pages" }
719
1605
  });
720
- return { content: [{
721
- type: "text",
722
- text: JSON.stringify({ pages }, null, 2)
723
- }] };
1606
+ return createStructuredTextResult({ pages });
724
1607
  } catch (error) {
725
1608
  const elapsed = durationMs(startedAt);
726
1609
  await emitDocsAgentTraceEvent(options.observability, {
@@ -744,6 +1627,7 @@ async function createDocsMcpServer(options) {
744
1627
  title: "List docs by section",
745
1628
  description: "List documentation pages grouped by section, optionally narrowed to one section.",
746
1629
  inputSchema: listDocsInputSchema,
1630
+ outputSchema: listDocsOutputSchema,
747
1631
  annotations: { readOnlyHint: true }
748
1632
  }, async ({ section, locale }) => {
749
1633
  const startedAt = nowMs();
@@ -765,7 +1649,7 @@ async function createDocsMcpServer(options) {
765
1649
  metadata: { tool: "list_docs" }
766
1650
  });
767
1651
  try {
768
- const docs = listDocsBySection(dedupePages(await options.source.getPages(locale)), {
1652
+ const docs = listDocsBySection(dedupePages(await getSourcePages(locale)), {
769
1653
  section,
770
1654
  entry: options.source.entry
771
1655
  });
@@ -775,38 +1659,281 @@ async function createDocsMcpServer(options) {
775
1659
  source: "mcp",
776
1660
  locale,
777
1661
  properties: {
778
- tool: "list_docs",
779
- section,
780
- resultCount: docs.resultCount,
781
- sectionCount: docs.sectionCount,
1662
+ tool: "list_docs",
1663
+ section,
1664
+ resultCount: docs.resultCount,
1665
+ sectionCount: docs.sectionCount,
1666
+ durationMs: elapsed
1667
+ }
1668
+ });
1669
+ trackMcpTool("list_docs", {
1670
+ locale,
1671
+ resultCount: docs.resultCount
1672
+ });
1673
+ await emitDocsAgentTraceEvent(options.observability, {
1674
+ type: "tool.result",
1675
+ source: "mcp",
1676
+ traceId: trace.traceId,
1677
+ parentSpanId: callSpanId,
1678
+ name: "list_docs",
1679
+ startedAt: trace.startedAt,
1680
+ endedAt: (/* @__PURE__ */ new Date()).toISOString(),
1681
+ durationMs: elapsed,
1682
+ status: "success",
1683
+ locale,
1684
+ outputPreview: {
1685
+ resultCount: docs.resultCount,
1686
+ sectionCount: docs.sectionCount
1687
+ },
1688
+ metadata: { tool: "list_docs" }
1689
+ });
1690
+ return createStructuredTextResult(docs);
1691
+ } catch (error) {
1692
+ const elapsed = durationMs(startedAt);
1693
+ await emitDocsAgentTraceEvent(options.observability, {
1694
+ type: "tool.error",
1695
+ source: "mcp",
1696
+ traceId: trace.traceId,
1697
+ parentSpanId: callSpanId,
1698
+ name: "list_docs",
1699
+ startedAt: trace.startedAt,
1700
+ endedAt: (/* @__PURE__ */ new Date()).toISOString(),
1701
+ durationMs: elapsed,
1702
+ status: "error",
1703
+ locale,
1704
+ outputPreview: { message: error instanceof Error ? error.message : "Unknown error" },
1705
+ metadata: { tool: "list_docs" }
1706
+ });
1707
+ throw error;
1708
+ }
1709
+ });
1710
+ if (resolved.tools.listTasks) server.registerTool("list_tasks", {
1711
+ title: "List documented tasks",
1712
+ description: "List pages with actionable agent contracts, optionally filtered by text or applicability.",
1713
+ inputSchema: listTasksInputSchema,
1714
+ outputSchema: listTasksOutputSchema,
1715
+ annotations: { readOnlyHint: true }
1716
+ }, async ({ query, framework, version, package: packageName, locale }) => {
1717
+ const startedAt = nowMs();
1718
+ const trace = createDocsAgentTraceContext("mcp.tool.list_tasks");
1719
+ const callSpanId = createDocsAgentTraceId("span");
1720
+ await emitDocsAgentTraceEvent(options.observability, {
1721
+ type: "tool.call",
1722
+ source: "mcp",
1723
+ traceId: trace.traceId,
1724
+ spanId: callSpanId,
1725
+ name: "list_tasks",
1726
+ startedAt: trace.startedAt,
1727
+ status: "started",
1728
+ locale,
1729
+ inputPreview: {
1730
+ queryLength: query?.length,
1731
+ framework,
1732
+ version,
1733
+ package: packageName
1734
+ },
1735
+ metadata: { tool: "list_tasks" }
1736
+ });
1737
+ try {
1738
+ const tasks = listDocsTasks(dedupePages(await getSourcePages(locale)), {
1739
+ query,
1740
+ framework,
1741
+ version,
1742
+ package: packageName
1743
+ });
1744
+ const result = {
1745
+ resultCount: tasks.length,
1746
+ tasks
1747
+ };
1748
+ const elapsed = durationMs(startedAt);
1749
+ await emitDocsAnalyticsEvent(options.analytics, {
1750
+ type: "mcp_tool",
1751
+ source: "mcp",
1752
+ locale,
1753
+ input: query ? { query } : void 0,
1754
+ properties: {
1755
+ tool: "list_tasks",
1756
+ framework,
1757
+ version,
1758
+ package: packageName,
1759
+ resultCount: tasks.length,
1760
+ durationMs: elapsed
1761
+ }
1762
+ });
1763
+ trackMcpTool("list_tasks", {
1764
+ locale,
1765
+ resultCount: tasks.length
1766
+ });
1767
+ await emitDocsAgentTraceEvent(options.observability, {
1768
+ type: "tool.result",
1769
+ source: "mcp",
1770
+ traceId: trace.traceId,
1771
+ parentSpanId: callSpanId,
1772
+ name: "list_tasks",
1773
+ startedAt: trace.startedAt,
1774
+ endedAt: (/* @__PURE__ */ new Date()).toISOString(),
1775
+ durationMs: elapsed,
1776
+ status: "success",
1777
+ locale,
1778
+ outputPreview: { resultCount: tasks.length },
1779
+ metadata: { tool: "list_tasks" }
1780
+ });
1781
+ return {
1782
+ structuredContent: result,
1783
+ content: [{
1784
+ type: "text",
1785
+ text: JSON.stringify(result, null, 2)
1786
+ }]
1787
+ };
1788
+ } catch (error) {
1789
+ const elapsed = durationMs(startedAt);
1790
+ await emitDocsAgentTraceEvent(options.observability, {
1791
+ type: "tool.error",
1792
+ source: "mcp",
1793
+ traceId: trace.traceId,
1794
+ parentSpanId: callSpanId,
1795
+ name: "list_tasks",
1796
+ startedAt: trace.startedAt,
1797
+ endedAt: (/* @__PURE__ */ new Date()).toISOString(),
1798
+ durationMs: elapsed,
1799
+ status: "error",
1800
+ locale,
1801
+ outputPreview: { message: error instanceof Error ? error.message : "Unknown error" },
1802
+ metadata: { tool: "list_tasks" }
1803
+ });
1804
+ throw error;
1805
+ }
1806
+ });
1807
+ if (resolved.tools.readTask) server.registerTool("read_task", {
1808
+ title: "Read a documented task",
1809
+ description: "Read the full structured agent contract for a page by slug or URL path.",
1810
+ inputSchema: readTaskInputSchema,
1811
+ outputSchema: readTaskOutputSchema,
1812
+ annotations: { readOnlyHint: true }
1813
+ }, async ({ path: requestedPath, locale }) => {
1814
+ const startedAt = nowMs();
1815
+ const trace = createDocsAgentTraceContext("mcp.tool.read_task");
1816
+ const callSpanId = createDocsAgentTraceId("span");
1817
+ await emitDocsAgentTraceEvent(options.observability, {
1818
+ type: "tool.call",
1819
+ source: "mcp",
1820
+ traceId: trace.traceId,
1821
+ spanId: callSpanId,
1822
+ name: "read_task",
1823
+ startedAt: trace.startedAt,
1824
+ status: "started",
1825
+ locale,
1826
+ inputPreview: {
1827
+ path: requestedPath,
1828
+ locale
1829
+ },
1830
+ metadata: { tool: "read_task" }
1831
+ });
1832
+ try {
1833
+ const page = findDocsPage(dedupePages(await getSourcePages(locale)), requestedPath, options.source.entry);
1834
+ const contract = normalizePageAgentFrontmatter(page?.agent);
1835
+ if (!page || !contract || !hasStructuredPageAgentContract(contract)) {
1836
+ const elapsed = durationMs(startedAt);
1837
+ const reason = page ? "contract_not_found" : "page_not_found";
1838
+ const errorResult = { error: page ? `The docs page matched "${requestedPath}", but it has no actionable agent contract.` : `No docs page matched "${requestedPath}".` };
1839
+ await emitDocsAnalyticsEvent(options.analytics, {
1840
+ type: "mcp_tool",
1841
+ source: "mcp",
1842
+ locale,
1843
+ properties: {
1844
+ tool: "read_task",
1845
+ path: requestedPath,
1846
+ found: false,
1847
+ reason,
1848
+ durationMs: elapsed
1849
+ }
1850
+ });
1851
+ trackMcpTool("read_task", {
1852
+ locale,
1853
+ resultCount: 0
1854
+ });
1855
+ await emitDocsAgentTraceEvent(options.observability, {
1856
+ type: "tool.error",
1857
+ source: "mcp",
1858
+ traceId: trace.traceId,
1859
+ parentSpanId: callSpanId,
1860
+ name: "read_task",
1861
+ startedAt: trace.startedAt,
1862
+ endedAt: (/* @__PURE__ */ new Date()).toISOString(),
1863
+ durationMs: elapsed,
1864
+ status: "error",
1865
+ locale,
1866
+ outputPreview: {
1867
+ found: false,
1868
+ path: requestedPath
1869
+ },
1870
+ metadata: {
1871
+ tool: "read_task",
1872
+ reason
1873
+ }
1874
+ });
1875
+ return {
1876
+ content: [{
1877
+ type: "text",
1878
+ text: JSON.stringify(errorResult, null, 2)
1879
+ }],
1880
+ isError: true
1881
+ };
1882
+ }
1883
+ const result = {
1884
+ page: {
1885
+ slug: page.slug,
1886
+ url: page.url,
1887
+ title: page.title,
1888
+ ...page.description ? { description: page.description } : {},
1889
+ ...page.sourcePath ? { sourcePath: page.sourcePath } : {},
1890
+ ...page.lastModified ? { lastModified: page.lastModified } : {}
1891
+ },
1892
+ contract
1893
+ };
1894
+ const elapsed = durationMs(startedAt);
1895
+ await emitDocsAnalyticsEvent(options.analytics, {
1896
+ type: "mcp_tool",
1897
+ source: "mcp",
1898
+ locale,
1899
+ path: page.url,
1900
+ properties: {
1901
+ tool: "read_task",
1902
+ requestedPath,
1903
+ slug: page.slug,
1904
+ found: true,
782
1905
  durationMs: elapsed
783
1906
  }
784
1907
  });
785
- trackMcpTool("list_docs", {
1908
+ trackMcpTool("read_task", {
786
1909
  locale,
787
- resultCount: docs.resultCount
1910
+ resultCount: 1
788
1911
  });
789
1912
  await emitDocsAgentTraceEvent(options.observability, {
790
1913
  type: "tool.result",
791
1914
  source: "mcp",
792
1915
  traceId: trace.traceId,
793
1916
  parentSpanId: callSpanId,
794
- name: "list_docs",
1917
+ name: "read_task",
795
1918
  startedAt: trace.startedAt,
796
1919
  endedAt: (/* @__PURE__ */ new Date()).toISOString(),
797
1920
  durationMs: elapsed,
798
1921
  status: "success",
799
1922
  locale,
1923
+ path: page.url,
800
1924
  outputPreview: {
801
- resultCount: docs.resultCount,
802
- sectionCount: docs.sectionCount
1925
+ found: true,
1926
+ slug: page.slug
803
1927
  },
804
- metadata: { tool: "list_docs" }
1928
+ metadata: { tool: "read_task" }
805
1929
  });
806
- return { content: [{
807
- type: "text",
808
- text: JSON.stringify(docs, null, 2)
809
- }] };
1930
+ return {
1931
+ structuredContent: result,
1932
+ content: [{
1933
+ type: "text",
1934
+ text: JSON.stringify(result, null, 2)
1935
+ }]
1936
+ };
810
1937
  } catch (error) {
811
1938
  const elapsed = durationMs(startedAt);
812
1939
  await emitDocsAgentTraceEvent(options.observability, {
@@ -814,14 +1941,14 @@ async function createDocsMcpServer(options) {
814
1941
  source: "mcp",
815
1942
  traceId: trace.traceId,
816
1943
  parentSpanId: callSpanId,
817
- name: "list_docs",
1944
+ name: "read_task",
818
1945
  startedAt: trace.startedAt,
819
1946
  endedAt: (/* @__PURE__ */ new Date()).toISOString(),
820
1947
  durationMs: elapsed,
821
1948
  status: "error",
822
1949
  locale,
823
1950
  outputPreview: { message: error instanceof Error ? error.message : "Unknown error" },
824
- metadata: { tool: "list_docs" }
1951
+ metadata: { tool: "read_task" }
825
1952
  });
826
1953
  throw error;
827
1954
  }
@@ -830,6 +1957,7 @@ async function createDocsMcpServer(options) {
830
1957
  title: "Get docs navigation",
831
1958
  description: "Return the documentation navigation tree for the current docs site.",
832
1959
  inputSchema: getNavigationInputSchema,
1960
+ outputSchema: navigationOutputSchema,
833
1961
  annotations: { readOnlyHint: true }
834
1962
  }, async ({ locale }) => {
835
1963
  const startedAt = nowMs();
@@ -848,7 +1976,8 @@ async function createDocsMcpServer(options) {
848
1976
  metadata: { tool: "get_navigation" }
849
1977
  });
850
1978
  try {
851
- const text = renderNavigationTree(await options.source.getNavigation(locale));
1979
+ const tree = await getSourceNavigation(locale);
1980
+ const text = renderNavigationTree(tree);
852
1981
  const elapsed = durationMs(startedAt);
853
1982
  await emitDocsAnalyticsEvent(options.analytics, {
854
1983
  type: "mcp_tool",
@@ -874,10 +2003,10 @@ async function createDocsMcpServer(options) {
874
2003
  outputPreview: { chars: text.length },
875
2004
  metadata: { tool: "get_navigation" }
876
2005
  });
877
- return { content: [{
878
- type: "text",
879
- text
880
- }] };
2006
+ return createStructuredTextResult({
2007
+ navigation: tree,
2008
+ markdown: text
2009
+ }, text);
881
2010
  } catch (error) {
882
2011
  const elapsed = durationMs(startedAt);
883
2012
  await emitDocsAgentTraceEvent(options.observability, {
@@ -901,6 +2030,7 @@ async function createDocsMcpServer(options) {
901
2030
  title: "Get docs config schema",
902
2031
  description: "Return structured docs.config.ts option metadata, optionally filtered by option path or query.",
903
2032
  inputSchema: getConfigSchemaInputSchema,
2033
+ outputSchema: configSchemaOutputSchema,
904
2034
  annotations: { readOnlyHint: true }
905
2035
  }, async ({ option, query }) => {
906
2036
  const startedAt = nowMs();
@@ -952,10 +2082,7 @@ async function createDocsMcpServer(options) {
952
2082
  outputPreview: { resultCount: schema.resultCount },
953
2083
  metadata: { tool: "get_config_schema" }
954
2084
  });
955
- return { content: [{
956
- type: "text",
957
- text: JSON.stringify(schema, null, 2)
958
- }] };
2085
+ return createStructuredTextResult(schema);
959
2086
  } catch (error) {
960
2087
  const elapsed = durationMs(startedAt);
961
2088
  await emitDocsAgentTraceEvent(options.observability, {
@@ -978,6 +2105,7 @@ async function createDocsMcpServer(options) {
978
2105
  title: "Search documentation",
979
2106
  description: "Search the docs by keyword across titles, descriptions, and page content.",
980
2107
  inputSchema: searchDocsInputSchema,
2108
+ outputSchema: searchDocsOutputSchema,
981
2109
  annotations: { readOnlyHint: true }
982
2110
  }, async ({ query, limit, locale }) => {
983
2111
  const startedAt = nowMs();
@@ -1002,7 +2130,7 @@ async function createDocsMcpServer(options) {
1002
2130
  });
1003
2131
  try {
1004
2132
  const results = await performDocsSearch({
1005
- pages: toSearchSourcePages(dedupePages(await options.source.getPages(locale))),
2133
+ pages: toSearchSourcePages(dedupePages(await getSourcePages(locale))),
1006
2134
  query,
1007
2135
  search: toolSearchConfig ?? true,
1008
2136
  locale,
@@ -1041,10 +2169,7 @@ async function createDocsMcpServer(options) {
1041
2169
  outputPreview: { resultCount: results.length },
1042
2170
  metadata: { tool: "search_docs" }
1043
2171
  });
1044
- return { content: [{
1045
- type: "text",
1046
- text: JSON.stringify({ results }, null, 2)
1047
- }] };
2172
+ return createStructuredTextResult({ results });
1048
2173
  } catch (error) {
1049
2174
  const elapsed = durationMs(startedAt);
1050
2175
  await emitDocsAgentTraceEvent(options.observability, {
@@ -1068,6 +2193,7 @@ async function createDocsMcpServer(options) {
1068
2193
  title: "Get docs code examples",
1069
2194
  description: "Return fenced code examples from the docs with parsed metadata such as title, framework, packageManager, and runnable.",
1070
2195
  inputSchema: getCodeExamplesInputSchema,
2196
+ outputSchema: codeExamplesOutputSchema,
1071
2197
  annotations: { readOnlyHint: true }
1072
2198
  }, async ({ query, path: requestedPath, framework, packageManager, language, runnable, limit, locale }) => {
1073
2199
  const startedAt = nowMs();
@@ -1095,7 +2221,7 @@ async function createDocsMcpServer(options) {
1095
2221
  metadata: { tool: "get_code_examples" }
1096
2222
  });
1097
2223
  try {
1098
- const pages = dedupePages(await options.source.getPages(locale));
2224
+ const pages = dedupePages(await getSourcePages(locale));
1099
2225
  const matchedPage = requestedPath ? findDocsPage(pages, requestedPath, options.source.entry) : null;
1100
2226
  const examples = filterDocsCodeExamples((requestedPath ? matchedPage ? [matchedPage] : [] : pages).flatMap((page) => extractDocsMcpCodeExamples(page)), {
1101
2227
  query,
@@ -1142,10 +2268,7 @@ async function createDocsMcpServer(options) {
1142
2268
  outputPreview: { resultCount: examples.length },
1143
2269
  metadata: { tool: "get_code_examples" }
1144
2270
  });
1145
- return { content: [{
1146
- type: "text",
1147
- text: JSON.stringify({ examples }, null, 2)
1148
- }] };
2271
+ return createStructuredTextResult({ examples });
1149
2272
  } catch (error) {
1150
2273
  const elapsed = durationMs(startedAt);
1151
2274
  await emitDocsAgentTraceEvent(options.observability, {
@@ -1165,12 +2288,115 @@ async function createDocsMcpServer(options) {
1165
2288
  throw error;
1166
2289
  }
1167
2290
  });
2291
+ if (resolved.tools.getContext) server.registerTool("get_context", {
2292
+ title: "Get budgeted docs context",
2293
+ description: "Build deterministic, section-level documentation context for a query within a conservative UTF-8 byte ceiling derived from the requested token budget, with source URLs and accounting metadata.",
2294
+ inputSchema: getContextInputSchema,
2295
+ outputSchema: contextOutputSchema,
2296
+ annotations: { readOnlyHint: true }
2297
+ }, async ({ query, framework, version, locale, tokenBudget }) => {
2298
+ const startedAt = nowMs();
2299
+ const trace = createDocsAgentTraceContext("mcp.tool.get_context");
2300
+ const callSpanId = createDocsAgentTraceId("span");
2301
+ await emitDocsAgentTraceEvent(options.observability, {
2302
+ type: "tool.call",
2303
+ source: "mcp",
2304
+ traceId: trace.traceId,
2305
+ spanId: callSpanId,
2306
+ name: "get_context",
2307
+ startedAt: trace.startedAt,
2308
+ status: "started",
2309
+ locale,
2310
+ inputPreview: {
2311
+ queryLength: query.length,
2312
+ framework,
2313
+ version,
2314
+ locale,
2315
+ tokenBudget
2316
+ },
2317
+ metadata: { tool: "get_context" }
2318
+ });
2319
+ try {
2320
+ const result = await buildDocsMcpContext({
2321
+ pages: dedupePages(await getSourcePages(locale)),
2322
+ query,
2323
+ framework,
2324
+ version,
2325
+ locale,
2326
+ tokenBudget,
2327
+ entry: options.source.entry,
2328
+ siteTitle: options.source.siteTitle
2329
+ });
2330
+ const elapsed = durationMs(startedAt);
2331
+ await emitDocsAnalyticsEvent(options.analytics, {
2332
+ type: "mcp_tool",
2333
+ source: "mcp",
2334
+ locale,
2335
+ input: { query },
2336
+ properties: {
2337
+ tool: "get_context",
2338
+ queryLength: query.length,
2339
+ framework,
2340
+ version,
2341
+ tokenBudget,
2342
+ usedUtf8Bytes: result.budget.usedUtf8Bytes,
2343
+ conservativeTokenUpperBound: result.budget.conservativeTokenUpperBound,
2344
+ truncated: result.budget.truncated,
2345
+ resultCount: result.resultCount,
2346
+ durationMs: elapsed
2347
+ }
2348
+ });
2349
+ trackMcpTool("get_context", {
2350
+ locale,
2351
+ resultCount: result.resultCount
2352
+ });
2353
+ await emitDocsAgentTraceEvent(options.observability, {
2354
+ type: "tool.result",
2355
+ source: "mcp",
2356
+ traceId: trace.traceId,
2357
+ parentSpanId: callSpanId,
2358
+ name: "get_context",
2359
+ startedAt: trace.startedAt,
2360
+ endedAt: (/* @__PURE__ */ new Date()).toISOString(),
2361
+ durationMs: elapsed,
2362
+ status: "success",
2363
+ locale,
2364
+ outputPreview: {
2365
+ resultCount: result.resultCount,
2366
+ candidateCount: result.candidateCount,
2367
+ utf8Bytes: result.budget.usedUtf8Bytes,
2368
+ conservativeTokenUpperBound: result.budget.conservativeTokenUpperBound,
2369
+ truncated: result.budget.truncated
2370
+ },
2371
+ metadata: { tool: "get_context" }
2372
+ });
2373
+ return createStructuredTextResult(result, result.context || JSON.stringify(result, null, 2));
2374
+ } catch (error) {
2375
+ const elapsed = durationMs(startedAt);
2376
+ await emitDocsAgentTraceEvent(options.observability, {
2377
+ type: "tool.error",
2378
+ source: "mcp",
2379
+ traceId: trace.traceId,
2380
+ parentSpanId: callSpanId,
2381
+ name: "get_context",
2382
+ startedAt: trace.startedAt,
2383
+ endedAt: (/* @__PURE__ */ new Date()).toISOString(),
2384
+ durationMs: elapsed,
2385
+ status: "error",
2386
+ locale,
2387
+ outputPreview: { message: error instanceof Error ? error.message : "Unknown error" },
2388
+ metadata: { tool: "get_context" }
2389
+ });
2390
+ throw error;
2391
+ }
2392
+ });
1168
2393
  if (resolved.tools.readPage) server.registerTool("read_page", {
1169
2394
  title: "Read a docs page",
1170
- description: "Read a documentation page by slug or URL path.",
2395
+ description: "Read a documentation page by slug or URL path, optionally selecting one heading and limiting returned characters.",
1171
2396
  inputSchema: readPageInputSchema,
2397
+ outputSchema: readPageOutputSchema,
1172
2398
  annotations: { readOnlyHint: true }
1173
- }, async ({ path: requestedPath, locale }) => {
2399
+ }, async ({ path: requestedPath, locale, section, maxChars }) => {
1174
2400
  const startedAt = nowMs();
1175
2401
  const trace = createDocsAgentTraceContext("mcp.tool.read_page");
1176
2402
  const callSpanId = createDocsAgentTraceId("span");
@@ -1185,12 +2411,14 @@ async function createDocsMcpServer(options) {
1185
2411
  locale,
1186
2412
  inputPreview: {
1187
2413
  path: requestedPath,
1188
- locale
2414
+ locale,
2415
+ section,
2416
+ maxChars
1189
2417
  },
1190
2418
  metadata: { tool: "read_page" }
1191
2419
  });
1192
2420
  try {
1193
- const page = findDocsPage(dedupePages(await options.source.getPages(locale)), requestedPath, options.source.entry);
2421
+ const page = findDocsPage(dedupePages(await getSourcePages(locale)), requestedPath, options.source.entry);
1194
2422
  if (!page) {
1195
2423
  const elapsed = durationMs(startedAt);
1196
2424
  await emitDocsAnalyticsEvent(options.analytics, {
@@ -1248,7 +2476,59 @@ async function createDocsMcpServer(options) {
1248
2476
  isError: true
1249
2477
  };
1250
2478
  }
1251
- const document = renderPageDocument(page);
2479
+ const fullDocument = renderPageDocument(page);
2480
+ const sectionDocument = getDocsMcpSourceMarkdown(page);
2481
+ const selectedSection = section ? findDocsMarkdownSection(sectionDocument, section) ?? (sectionDocument !== fullDocument ? findDocsMarkdownSection(fullDocument, section) : void 0) : void 0;
2482
+ if (section && !selectedSection) {
2483
+ const sourceSections = parseDocsMarkdownSections(sectionDocument);
2484
+ const availableSections = (sourceSections.length > 0 ? sourceSections : parseDocsMarkdownSections(fullDocument)).map((item) => ({
2485
+ title: item.title,
2486
+ anchor: item.anchor
2487
+ }));
2488
+ const errorText = renderBoundedDocsMcpSectionError({
2489
+ requestedSection: section,
2490
+ pageUrl: page.url,
2491
+ availableSections,
2492
+ maxChars
2493
+ });
2494
+ const elapsed = durationMs(startedAt);
2495
+ trackMcpTool("read_page", {
2496
+ locale,
2497
+ resultCount: 0
2498
+ });
2499
+ await emitDocsAgentTraceEvent(options.observability, {
2500
+ type: "tool.error",
2501
+ source: "mcp",
2502
+ traceId: trace.traceId,
2503
+ parentSpanId: callSpanId,
2504
+ name: "read_page",
2505
+ startedAt: trace.startedAt,
2506
+ endedAt: (/* @__PURE__ */ new Date()).toISOString(),
2507
+ durationMs: elapsed,
2508
+ status: "error",
2509
+ locale,
2510
+ path: page.url,
2511
+ outputPreview: {
2512
+ found: true,
2513
+ sectionFound: false,
2514
+ section
2515
+ },
2516
+ metadata: {
2517
+ tool: "read_page",
2518
+ reason: "section_not_found"
2519
+ }
2520
+ });
2521
+ return {
2522
+ content: [{
2523
+ type: "text",
2524
+ text: errorText
2525
+ }],
2526
+ isError: true
2527
+ };
2528
+ }
2529
+ const selectedDocument = selectedSection?.content ?? fullDocument;
2530
+ const limitedDocument = limitDocsMcpText(selectedDocument, maxChars);
2531
+ const document = limitedDocument.text;
1252
2532
  const elapsed = durationMs(startedAt);
1253
2533
  await emitDocsAnalyticsEvent(options.analytics, {
1254
2534
  type: "agent_read",
@@ -1261,7 +2541,10 @@ async function createDocsMcpServer(options) {
1261
2541
  requestedPath,
1262
2542
  slug: page.slug,
1263
2543
  found: true,
2544
+ section: selectedSection?.title,
1264
2545
  contentLength: document.length,
2546
+ totalContentLength: selectedDocument.length,
2547
+ truncated: limitedDocument.truncated,
1265
2548
  durationMs: elapsed
1266
2549
  }
1267
2550
  });
@@ -1275,7 +2558,10 @@ async function createDocsMcpServer(options) {
1275
2558
  requestedPath,
1276
2559
  slug: page.slug,
1277
2560
  found: true,
2561
+ section: selectedSection?.title,
1278
2562
  contentLength: document.length,
2563
+ totalContentLength: selectedDocument.length,
2564
+ truncated: limitedDocument.truncated,
1279
2565
  durationMs: elapsed
1280
2566
  }
1281
2567
  });
@@ -1298,14 +2584,22 @@ async function createDocsMcpServer(options) {
1298
2584
  outputPreview: {
1299
2585
  found: true,
1300
2586
  chars: document.length,
2587
+ totalChars: selectedDocument.length,
2588
+ truncated: limitedDocument.truncated,
2589
+ section: selectedSection?.title,
1301
2590
  slug: page.slug
1302
2591
  },
1303
2592
  metadata: { tool: "read_page" }
1304
2593
  });
1305
- return { content: [{
1306
- type: "text",
1307
- text: document
1308
- }] };
2594
+ return createStructuredTextResult({
2595
+ page: toStructuredDocsMcpPage(page),
2596
+ document,
2597
+ section: selectedSection?.title,
2598
+ anchor: selectedSection?.anchor,
2599
+ chars: document.length,
2600
+ totalChars: selectedDocument.length,
2601
+ truncated: limitedDocument.truncated
2602
+ }, document);
1309
2603
  } catch (error) {
1310
2604
  const elapsed = durationMs(startedAt);
1311
2605
  await emitDocsAgentTraceEvent(options.observability, {
@@ -1348,10 +2642,14 @@ function createDocsMcpHttpHandler(options) {
1348
2642
  id: readJsonRpcId(await parseJsonBody(request)),
1349
2643
  data: { reason: "mcp_disabled" }
1350
2644
  }),
1351
- DELETE: async () => createJsonErrorResponse(404, disabledMessage)
2645
+ DELETE: async () => createJsonErrorResponse(404, disabledMessage),
2646
+ OPTIONS: async () => createJsonErrorResponse(404, disabledMessage)
1352
2647
  };
1353
- async function createStatelessTransport() {
1354
- const server = await createDocsMcpServer(options);
2648
+ async function createStatelessTransport(requestContext) {
2649
+ const server = await createDocsMcpServer({
2650
+ ...options,
2651
+ requestContext
2652
+ });
1355
2653
  const transport = new WebStandardStreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
1356
2654
  await server.connect(transport);
1357
2655
  return {
@@ -1362,15 +2660,38 @@ function createDocsMcpHttpHandler(options) {
1362
2660
  async function handle(request) {
1363
2661
  const url = new URL(request.url);
1364
2662
  const method = request.method.toUpperCase();
1365
- const sessionId = request.headers.get("mcp-session-id") ?? request.headers.get("Mcp-Session-Id");
1366
- let parsedBody;
1367
- let bodyParseFailed = false;
1368
- if (method === "POST") try {
1369
- parsedBody = await request.clone().json();
2663
+ const security = resolved.security ?? resolveDocsMcpSecurityConfig();
2664
+ const prepared = await prepareDocsMcpHttpRequest(request, security.maxBodyBytes);
2665
+ if (prepared.status === "too-large") return createMcpRequestTooLargeResponse(security.maxBodyBytes);
2666
+ request = prepared.request;
2667
+ let originAllowed;
2668
+ try {
2669
+ originAllowed = await isDocsMcpOriginAllowed(request.clone(), security.allowedOrigins);
1370
2670
  } catch {
1371
- bodyParseFailed = true;
1372
- parsedBody = void 0;
2671
+ return createMcpHttpSecurityErrorResponse(500, "MCP Origin policy failed");
1373
2672
  }
2673
+ if (!originAllowed) return createMcpHttpSecurityErrorResponse(403, "Forbidden Origin");
2674
+ if (method === "OPTIONS") return createDocsMcpOptionsResponse(request, security.cors);
2675
+ const withCors = (response) => applyDocsMcpCorsHeaders(response, request, security.cors);
2676
+ let auth;
2677
+ if (security.authenticate) {
2678
+ let authentication;
2679
+ try {
2680
+ authentication = await security.authenticate({
2681
+ request: request.clone(),
2682
+ pathname: url.pathname
2683
+ });
2684
+ } catch {
2685
+ return withCors(createMcpHttpSecurityErrorResponse(500, "MCP authentication failed"));
2686
+ }
2687
+ if (authentication instanceof Response) return withCors(authentication);
2688
+ if (authentication === null || authentication === void 0) return withCors(createMcpHttpSecurityErrorResponse(401, "Unauthorized"));
2689
+ if (!isDocsMcpAuthPrincipal(authentication)) return withCors(createMcpHttpSecurityErrorResponse(500, "MCP authentication returned an invalid principal"));
2690
+ auth = authentication;
2691
+ }
2692
+ const sessionId = request.headers.get("mcp-session-id") ?? request.headers.get("Mcp-Session-Id");
2693
+ const parsedBody = prepared.parsedBody;
2694
+ const bodyParseFailed = prepared.bodyParseFailed;
1374
2695
  const initializeRequest = method === "POST" && parsedBody && isInitializeRequest(parsedBody);
1375
2696
  emitDocsTelemetryProjectEvent(telemetryConfig, {
1376
2697
  framework: telemetryFramework,
@@ -1397,24 +2718,220 @@ function createDocsMcpHttpHandler(options) {
1397
2718
  initialize: Boolean(initializeRequest)
1398
2719
  }
1399
2720
  });
1400
- if (method === "POST" && bodyParseFailed) return createJsonRpcErrorResponse({
2721
+ if (method === "POST" && bodyParseFailed) return withCors(createJsonRpcErrorResponse({
1401
2722
  status: 400,
1402
2723
  code: -32700,
1403
2724
  message: "Parse error: Invalid JSON"
1404
- });
1405
- return (await createStatelessTransport()).transport.handleRequest(request, parsedBody === void 0 ? void 0 : { parsedBody });
2725
+ }));
2726
+ return withCors(await (await createStatelessTransport({
2727
+ transport: "http",
2728
+ request: request.clone(),
2729
+ auth
2730
+ })).transport.handleRequest(request, parsedBody === void 0 ? void 0 : { parsedBody }));
1406
2731
  }
1407
2732
  return {
1408
2733
  GET: async ({ request }) => handle(request),
1409
2734
  POST: async ({ request }) => handle(request),
1410
- DELETE: async ({ request }) => handle(request)
2735
+ DELETE: async ({ request }) => handle(request),
2736
+ OPTIONS: async ({ request }) => handle(request)
1411
2737
  };
1412
2738
  }
1413
2739
  async function runDocsMcpStdio(options) {
1414
- const server = await createDocsMcpServer(options);
2740
+ const server = await createDocsMcpServer({
2741
+ ...options,
2742
+ requestContext: { transport: "stdio" }
2743
+ });
1415
2744
  const transport = new StdioServerTransport();
1416
2745
  await server.connect(transport);
1417
2746
  }
2747
+ async function isDocsMcpOriginAllowed(request, allowedOrigins) {
2748
+ const origin = request.headers.get("origin");
2749
+ if (!origin) return true;
2750
+ if (typeof allowedOrigins === "function") return allowedOrigins({
2751
+ origin,
2752
+ request
2753
+ });
2754
+ const normalizedOrigin = normalizeHttpOrigin(origin);
2755
+ if (allowedOrigins === "same-origin") return normalizedOrigin === new URL(request.url).origin;
2756
+ return allowedOrigins.some((allowedOrigin) => normalizeHttpOrigin(allowedOrigin) === normalizedOrigin);
2757
+ }
2758
+ function normalizeHttpOrigin(value) {
2759
+ try {
2760
+ return new URL(value).origin;
2761
+ } catch {
2762
+ return value.trim();
2763
+ }
2764
+ }
2765
+ function isDocsMcpAuthPrincipal(value) {
2766
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
2767
+ const principal = value;
2768
+ if (typeof principal.id !== "string" || principal.id.trim().length === 0) return false;
2769
+ if (principal.scopes !== void 0 && (!Array.isArray(principal.scopes) || principal.scopes.some((scope) => typeof scope !== "string"))) return false;
2770
+ return principal.claims === void 0 || typeof principal.claims === "object" && principal.claims !== null && !Array.isArray(principal.claims);
2771
+ }
2772
+ async function prepareDocsMcpHttpRequest(request, maxBodyBytes) {
2773
+ if (request.method.toUpperCase() !== "POST") return {
2774
+ status: "ok",
2775
+ request,
2776
+ bodyParseFailed: false
2777
+ };
2778
+ if (isContentLengthOverLimit(request, maxBodyBytes)) return { status: "too-large" };
2779
+ const body = request.body;
2780
+ if (!body) return {
2781
+ status: "ok",
2782
+ request,
2783
+ bodyParseFailed: true
2784
+ };
2785
+ const reader = body.getReader();
2786
+ const chunks = [];
2787
+ let byteLength = 0;
2788
+ while (true) {
2789
+ const { done, value } = await reader.read();
2790
+ if (done) break;
2791
+ byteLength += value.byteLength;
2792
+ if (byteLength > maxBodyBytes) {
2793
+ reader.cancel();
2794
+ return { status: "too-large" };
2795
+ }
2796
+ chunks.push(value);
2797
+ }
2798
+ const bodyBytes = new Uint8Array(byteLength);
2799
+ let offset = 0;
2800
+ for (const chunk of chunks) {
2801
+ bodyBytes.set(chunk, offset);
2802
+ offset += chunk.byteLength;
2803
+ }
2804
+ const boundedRequest = new Request(request.url, {
2805
+ method: request.method,
2806
+ headers: new Headers(request.headers),
2807
+ body: bodyBytes,
2808
+ redirect: request.redirect,
2809
+ signal: request.signal
2810
+ });
2811
+ try {
2812
+ return {
2813
+ status: "ok",
2814
+ request: boundedRequest,
2815
+ parsedBody: JSON.parse(new TextDecoder().decode(bodyBytes)),
2816
+ bodyParseFailed: false
2817
+ };
2818
+ } catch {
2819
+ return {
2820
+ status: "ok",
2821
+ request: boundedRequest,
2822
+ bodyParseFailed: true
2823
+ };
2824
+ }
2825
+ }
2826
+ function isContentLengthOverLimit(request, maxBodyBytes) {
2827
+ const rawContentLength = request.headers.get("content-length");
2828
+ if (rawContentLength === null) return false;
2829
+ const contentLength = Number(rawContentLength);
2830
+ return Number.isFinite(contentLength) && contentLength > maxBodyBytes;
2831
+ }
2832
+ const DOCS_MCP_CORS_METHODS = [
2833
+ "GET",
2834
+ "POST",
2835
+ "DELETE",
2836
+ "OPTIONS"
2837
+ ];
2838
+ const DOCS_MCP_CORS_REQUEST_METHODS = new Set([
2839
+ "GET",
2840
+ "POST",
2841
+ "DELETE"
2842
+ ]);
2843
+ function createDocsMcpOptionsResponse(request, cors) {
2844
+ const allow = DOCS_MCP_CORS_METHODS.join(", ");
2845
+ if (!request.headers.get("origin") || !cors.enabled) return new Response(null, {
2846
+ status: 204,
2847
+ headers: { Allow: allow }
2848
+ });
2849
+ const requestedMethod = request.headers.get("access-control-request-method")?.trim().toUpperCase();
2850
+ if (requestedMethod && !DOCS_MCP_CORS_REQUEST_METHODS.has(requestedMethod)) return applyDocsMcpPreflightCorsHeaders(createMcpHttpSecurityErrorResponse(405, "CORS request method is not allowed"), request, cors);
2851
+ const allowedHeaders = new Set(cors.allowedHeaders.map((header) => header.toLowerCase()));
2852
+ const rejectedHeader = parseCorsRequestedHeaders(request.headers.get("access-control-request-headers")).find((header) => !allowedHeaders.has(header.toLowerCase()));
2853
+ if (rejectedHeader) return applyDocsMcpPreflightCorsHeaders(createMcpHttpSecurityErrorResponse(403, `CORS request header is not allowed: ${rejectedHeader}`), request, cors);
2854
+ return applyDocsMcpPreflightCorsHeaders(new Response(null, {
2855
+ status: 204,
2856
+ headers: { Allow: allow }
2857
+ }), request, cors);
2858
+ }
2859
+ function parseCorsRequestedHeaders(value) {
2860
+ if (!value) return [];
2861
+ return value.split(",").map((header) => header.trim()).filter(Boolean);
2862
+ }
2863
+ function applyDocsMcpPreflightCorsHeaders(response, request, cors) {
2864
+ const headers = buildDocsMcpCorsHeaders(response.headers, request, cors);
2865
+ headers.set("Access-Control-Allow-Methods", DOCS_MCP_CORS_METHODS.join(", "));
2866
+ headers.set("Access-Control-Allow-Headers", cors.allowedHeaders.join(", "));
2867
+ headers.set("Access-Control-Max-Age", String(cors.maxAgeSeconds));
2868
+ appendVaryHeader(headers, "Access-Control-Request-Method");
2869
+ appendVaryHeader(headers, "Access-Control-Request-Headers");
2870
+ return cloneResponseWithHeaders(response, headers);
2871
+ }
2872
+ function applyDocsMcpCorsHeaders(response, request, cors) {
2873
+ if (!request.headers.has("origin") || !cors.enabled) return response;
2874
+ const headers = buildDocsMcpCorsHeaders(response.headers, request, cors);
2875
+ if (!headers.has("Access-Control-Allow-Origin")) return response;
2876
+ if (cors.exposedHeaders.length > 0) headers.set("Access-Control-Expose-Headers", cors.exposedHeaders.join(", "));
2877
+ return cloneResponseWithHeaders(response, headers);
2878
+ }
2879
+ function buildDocsMcpCorsHeaders(source, request, cors) {
2880
+ const headers = new Headers(source);
2881
+ headers.delete("Access-Control-Allow-Origin");
2882
+ headers.delete("Access-Control-Allow-Credentials");
2883
+ const origin = serializeCorsOrigin(request.headers.get("origin"));
2884
+ if (!origin) return headers;
2885
+ headers.set("Access-Control-Allow-Origin", origin);
2886
+ if (cors.allowCredentials) headers.set("Access-Control-Allow-Credentials", "true");
2887
+ else headers.delete("Access-Control-Allow-Credentials");
2888
+ appendVaryHeader(headers, "Origin");
2889
+ return headers;
2890
+ }
2891
+ function serializeCorsOrigin(value) {
2892
+ if (!value) return null;
2893
+ if (value.trim() === "null") return "null";
2894
+ try {
2895
+ const origin = new URL(value).origin;
2896
+ return origin === "null" ? null : origin;
2897
+ } catch {
2898
+ return null;
2899
+ }
2900
+ }
2901
+ function appendVaryHeader(headers, value) {
2902
+ const values = (headers.get("Vary") ?? "").split(",").map((entry) => entry.trim()).filter(Boolean);
2903
+ if (!values.some((entry) => entry.toLowerCase() === value.toLowerCase())) values.push(value);
2904
+ headers.set("Vary", values.join(", "));
2905
+ }
2906
+ function cloneResponseWithHeaders(response, headers) {
2907
+ return new Response(response.body, {
2908
+ status: response.status,
2909
+ statusText: response.statusText,
2910
+ headers
2911
+ });
2912
+ }
2913
+ function createMcpRequestTooLargeResponse(maxBodyBytes) {
2914
+ const response = createJsonRpcErrorResponse({
2915
+ status: 413,
2916
+ code: -32e3,
2917
+ message: `Request body exceeds the ${maxBodyBytes} byte limit`,
2918
+ data: {
2919
+ reason: "request_too_large",
2920
+ maxBodyBytes
2921
+ }
2922
+ });
2923
+ response.headers.set("Cache-Control", "no-store");
2924
+ return response;
2925
+ }
2926
+ function createMcpHttpSecurityErrorResponse(status, error) {
2927
+ return new Response(JSON.stringify({ error }), {
2928
+ status,
2929
+ headers: {
2930
+ "Content-Type": "application/json",
2931
+ "Cache-Control": "no-store"
2932
+ }
2933
+ });
2934
+ }
1418
2935
  function createJsonErrorResponse(status, error) {
1419
2936
  return new Response(JSON.stringify({ error }), {
1420
2937
  status,
@@ -1570,9 +3087,14 @@ function scanFilesystemDocsPages(contentDirAbs, entry, rootDir) {
1570
3087
  title,
1571
3088
  description: data.description,
1572
3089
  relatedInput: data.related,
3090
+ agent: normalizePageAgentFrontmatter(data.agent),
1573
3091
  icon: data.icon,
1574
3092
  sourcePath: path.relative(rootDir, full).replace(/\\/g, "/"),
1575
3093
  lastModified: stat.mtime.toISOString(),
3094
+ locale: typeof data.locale === "string" ? data.locale : void 0,
3095
+ framework: typeof data.framework === "string" ? data.framework : void 0,
3096
+ version: typeof data.version === "string" ? data.version : void 0,
3097
+ tags: Array.isArray(data.tags) ? data.tags.filter((tag) => typeof tag === "string") : void 0,
1576
3098
  content: stripMarkdownForMcp(humanRawContent),
1577
3099
  rawContent: humanRawContent,
1578
3100
  agentFallbackContent: pageAgentContent,
@@ -1714,15 +3236,20 @@ function toSearchSourcePages(pages) {
1714
3236
  rawContent: page.agentRawContent ?? page.agentFallbackRawContent ?? page.rawContent,
1715
3237
  sourcePath: page.sourcePath,
1716
3238
  lastModified: page.lastModified,
3239
+ locale: page.locale,
3240
+ framework: page.framework,
3241
+ version: page.version,
3242
+ tags: page.tags,
1717
3243
  agentContent: page.agentContent,
1718
3244
  agentRawContent: page.agentRawContent,
1719
3245
  agentFallbackContent: page.agentFallbackContent,
1720
3246
  agentFallbackRawContent: page.agentFallbackRawContent,
1721
3247
  description: page.description,
1722
- related: page.related
3248
+ related: page.related,
3249
+ agent: page.agent
1723
3250
  }));
1724
3251
  }
1725
- function getDocsConfigSchema(filters) {
3252
+ function getDocsConfigSchema(filters = {}) {
1726
3253
  const option = filters.option?.trim();
1727
3254
  const query = filters.query?.trim();
1728
3255
  let options = DOCS_CONFIG_SCHEMA_OPTIONS.map(cloneConfigSchemaOption);
@@ -1738,12 +3265,13 @@ function getDocsConfigSchema(filters) {
1738
3265
  } : void 0,
1739
3266
  resultCount: countConfigSchemaOptions(options),
1740
3267
  options,
1741
- examples: DOCS_CONFIG_SCHEMA_EXAMPLES
3268
+ examples: DOCS_CONFIG_SCHEMA_EXAMPLES.map((example) => ({ ...example }))
1742
3269
  };
1743
3270
  }
1744
3271
  function cloneConfigSchemaOption(option) {
1745
3272
  return {
1746
3273
  ...option,
3274
+ values: option.values ? [...option.values] : void 0,
1747
3275
  children: option.children?.map(cloneConfigSchemaOption)
1748
3276
  };
1749
3277
  }
@@ -1803,13 +3331,24 @@ function resolveMcpToolSearchConfig(search, route) {
1803
3331
  chunking: config.chunking
1804
3332
  };
1805
3333
  }
3334
+ function toAgentContractSummary(value) {
3335
+ const agent = normalizePageAgentFrontmatter(value);
3336
+ const hasContract = hasStructuredPageAgentContract(agent);
3337
+ return {
3338
+ hasContract,
3339
+ ...hasContract && agent?.task ? { task: agent.task } : {},
3340
+ ...hasContract && agent?.outcome ? { outcome: agent.outcome } : {},
3341
+ ...hasContract && agent?.appliesTo ? { appliesTo: agent.appliesTo } : {}
3342
+ };
3343
+ }
1806
3344
  function toPageSummaries(pages) {
1807
3345
  return pages.map((page) => ({
1808
3346
  slug: page.slug,
1809
3347
  url: page.url,
1810
3348
  title: page.title,
1811
3349
  description: page.description,
1812
- icon: page.icon
3350
+ icon: page.icon,
3351
+ agent: toAgentContractSummary(page.agent)
1813
3352
  }));
1814
3353
  }
1815
3354
  function toDocsListPageSummary(page) {
@@ -1818,11 +3357,47 @@ function toDocsListPageSummary(page) {
1818
3357
  url: page.url,
1819
3358
  title: page.title,
1820
3359
  description: page.description,
3360
+ agent: toAgentContractSummary(page.agent),
1821
3361
  icon: page.icon,
1822
3362
  sourcePath: page.sourcePath,
1823
3363
  lastModified: page.lastModified
1824
3364
  };
1825
3365
  }
3366
+ function listDocsTasks(pages, filters) {
3367
+ const query = filters.query?.toLowerCase();
3368
+ const applicabilityFilters = [
3369
+ ["framework", filters.framework],
3370
+ ["version", filters.version],
3371
+ ["package", filters.package]
3372
+ ];
3373
+ return pages.flatMap((page) => {
3374
+ const agent = normalizePageAgentFrontmatter(page.agent);
3375
+ if (!agent || !hasStructuredPageAgentContract(agent)) return [];
3376
+ for (const [field, expected] of applicabilityFilters) {
3377
+ if (!expected) continue;
3378
+ const actualValue = agent.appliesTo?.[field];
3379
+ if (!(typeof actualValue === "string" ? [actualValue] : actualValue ?? []).some((value) => value.toLowerCase() === expected.toLowerCase())) return [];
3380
+ }
3381
+ if (query) {
3382
+ if (![
3383
+ page.slug,
3384
+ page.url,
3385
+ page.title,
3386
+ page.description,
3387
+ JSON.stringify(agent)
3388
+ ].filter(Boolean).join("\n").toLowerCase().includes(query)) return [];
3389
+ }
3390
+ return [{
3391
+ slug: page.slug,
3392
+ url: page.url,
3393
+ title: page.title,
3394
+ ...page.description ? { description: page.description } : {},
3395
+ ...agent.task ? { task: agent.task } : {},
3396
+ ...agent.outcome ? { outcome: agent.outcome } : {},
3397
+ ...agent.appliesTo ? { appliesTo: agent.appliesTo } : {}
3398
+ }];
3399
+ });
3400
+ }
1826
3401
  function listDocsBySection(pages, filters) {
1827
3402
  const allPages = pages.map(toDocsListPageSummary);
1828
3403
  const tree = buildDocsSectionTree(pages);
@@ -2113,6 +3688,288 @@ function getCodeExampleSearchText(example) {
2113
3688
  example.code
2114
3689
  ].filter((value) => typeof value === "string").join("\n");
2115
3690
  }
3691
+ function limitDocsMcpText(value, maxChars) {
3692
+ if (maxChars === void 0 || value.length <= maxChars) return {
3693
+ text: value,
3694
+ truncated: false
3695
+ };
3696
+ if (maxChars <= 1) return {
3697
+ text: value.slice(0, maxChars),
3698
+ truncated: true
3699
+ };
3700
+ const suffix = "…";
3701
+ const available = Math.max(0, maxChars - 1);
3702
+ let selected = value.slice(0, available);
3703
+ const paragraphBreak = selected.lastIndexOf("\n\n");
3704
+ const lineBreak = selected.lastIndexOf("\n");
3705
+ const preferredBreak = paragraphBreak >= available * .6 ? paragraphBreak : lineBreak;
3706
+ if (preferredBreak >= available * .6) selected = selected.slice(0, preferredBreak);
3707
+ return {
3708
+ text: `${selected.trimEnd()}${suffix}`.slice(0, maxChars),
3709
+ truncated: true
3710
+ };
3711
+ }
3712
+ function docsMcpUtf8Bytes(value) {
3713
+ return UTF8_ENCODER.encode(value).byteLength;
3714
+ }
3715
+ function limitDocsMcpUtf8Bytes(value, maxUtf8Bytes) {
3716
+ if (docsMcpUtf8Bytes(value) <= maxUtf8Bytes) return {
3717
+ text: value,
3718
+ truncated: false
3719
+ };
3720
+ if (maxUtf8Bytes <= 0) return {
3721
+ text: "",
3722
+ truncated: true
3723
+ };
3724
+ const suffix = "…";
3725
+ const suffixBytes = docsMcpUtf8Bytes(suffix);
3726
+ const availableBytes = Math.max(0, maxUtf8Bytes - suffixBytes);
3727
+ let selected = "";
3728
+ let selectedBytes = 0;
3729
+ for (const character of value) {
3730
+ const characterBytes = docsMcpUtf8Bytes(character);
3731
+ if (selectedBytes + characterBytes > availableBytes) break;
3732
+ selected += character;
3733
+ selectedBytes += characterBytes;
3734
+ }
3735
+ const paragraphBreak = selected.lastIndexOf("\n\n");
3736
+ const lineBreak = selected.lastIndexOf("\n");
3737
+ const preferredBreak = paragraphBreak >= selected.length * .6 ? paragraphBreak : lineBreak;
3738
+ if (preferredBreak >= selected.length * .6) selected = selected.slice(0, preferredBreak);
3739
+ if (suffixBytes > maxUtf8Bytes) return {
3740
+ text: ".".repeat(maxUtf8Bytes),
3741
+ truncated: true
3742
+ };
3743
+ return {
3744
+ text: `${selected.trimEnd()}${suffix}`,
3745
+ truncated: true
3746
+ };
3747
+ }
3748
+ function renderBoundedDocsMcpSectionError(options) {
3749
+ const maxChars = options.maxChars ?? Number.POSITIVE_INFINITY;
3750
+ const shorten = (value, max) => limitDocsMcpText(value, max).text;
3751
+ const availableSections = [];
3752
+ const base = {
3753
+ error: "section_not_found",
3754
+ message: `No section matched "${shorten(options.requestedSection, 80)}" in "${shorten(options.pageUrl, 80)}".`,
3755
+ availableSections,
3756
+ truncated: false
3757
+ };
3758
+ const serialize = () => JSON.stringify(base, null, 2);
3759
+ for (const section of options.availableSections) {
3760
+ availableSections.push({
3761
+ title: shorten(section.title, 80),
3762
+ anchor: shorten(section.anchor, 80)
3763
+ });
3764
+ if (serialize().length > maxChars) {
3765
+ availableSections.pop();
3766
+ base.truncated = true;
3767
+ break;
3768
+ }
3769
+ }
3770
+ if (availableSections.length < options.availableSections.length) base.truncated = true;
3771
+ if (serialize().length <= maxChars) return serialize();
3772
+ const minimalText = JSON.stringify({
3773
+ error: "section_not_found",
3774
+ message: "The requested section was not found.",
3775
+ availableSections: [],
3776
+ truncated: true
3777
+ });
3778
+ if (minimalText.length <= maxChars) return minimalText;
3779
+ return JSON.stringify({ error: "section_not_found" });
3780
+ }
3781
+ function toStructuredDocsMcpPage(page) {
3782
+ return {
3783
+ slug: page.slug,
3784
+ url: page.url,
3785
+ title: page.title,
3786
+ description: page.description,
3787
+ related: page.related,
3788
+ icon: page.icon,
3789
+ sourcePath: page.sourcePath,
3790
+ lastModified: page.lastModified,
3791
+ locale: page.locale,
3792
+ framework: page.framework,
3793
+ version: page.version,
3794
+ tags: page.tags
3795
+ };
3796
+ }
3797
+ function getDocsMcpSourceMarkdown(page) {
3798
+ return page.agentRawContent ?? page.agentFallbackRawContent ?? page.rawContent ?? page.agentContent ?? page.agentFallbackContent ?? page.content;
3799
+ }
3800
+ function resolveDocsMcpScopeField(pageValue, contractValue, filter, overlaps, matchesFilter) {
3801
+ const pageValues = normalizeAgentScopeValues(pageValue);
3802
+ const contractValues = normalizeAgentScopeValues(contractValue);
3803
+ const conflict = pageValues.length > 0 && contractValues.length > 0 && !pageValues.some((topLevel) => contractValues.some((contract) => overlaps(topLevel, contract)));
3804
+ const pageMatches = !filter || pageValues.length === 0 || pageValues.some((candidate) => matchesFilter(filter, candidate));
3805
+ const contractMatches = !filter || contractValues.length === 0 || contractValues.some((candidate) => matchesFilter(filter, candidate));
3806
+ const matchedContract = filter ? contractValues.find((candidate) => matchesFilter(filter, candidate)) : void 0;
3807
+ return {
3808
+ conflict,
3809
+ matches: !conflict && pageMatches && contractMatches,
3810
+ value: pageValues[0] ?? matchedContract ?? (contractValues.length === 1 ? contractValues[0] : void 0)
3811
+ };
3812
+ }
3813
+ function resolveDocsMcpEffectiveScope(page, filters) {
3814
+ const framework = resolveDocsMcpScopeField(page.framework, page.agent?.appliesTo?.framework, filters.framework, (left, right) => normalizeAgentFramework(left) === normalizeAgentFramework(right), (filter, candidate) => normalizeAgentFramework(filter) === normalizeAgentFramework(candidate));
3815
+ const version = resolveDocsMcpScopeField(page.version, page.agent?.appliesTo?.version, filters.version, agentVersionConstraintsOverlap, agentVersionConstraintMatches);
3816
+ const locale = page.locale?.trim() || void 0;
3817
+ const localeMatches = !filters.locale || !locale || normalizeAgentLocale(locale) === normalizeAgentLocale(filters.locale);
3818
+ return {
3819
+ framework: framework.value,
3820
+ version: version.value,
3821
+ locale,
3822
+ conflict: framework.conflict || version.conflict,
3823
+ matches: framework.matches && version.matches && localeMatches
3824
+ };
3825
+ }
3826
+ function getDocsMcpResultPageUrl(value) {
3827
+ return value.split("#", 1)[0] ?? value;
3828
+ }
3829
+ function getDocsMcpResultAnchor(value) {
3830
+ const anchor = value.split("#", 2)[1];
3831
+ if (!anchor) return void 0;
3832
+ try {
3833
+ return decodeURIComponent(anchor);
3834
+ } catch {
3835
+ return anchor;
3836
+ }
3837
+ }
3838
+ async function buildDocsMcpContext(options) {
3839
+ const scopedPageEntries = options.pages.flatMap((page) => {
3840
+ const scope = resolveDocsMcpEffectiveScope(page, {
3841
+ framework: options.framework,
3842
+ version: options.version,
3843
+ locale: options.locale
3844
+ });
3845
+ return scope.matches && !scope.conflict ? [{
3846
+ page,
3847
+ scope
3848
+ }] : [];
3849
+ });
3850
+ const scopedPages = scopedPageEntries.map(({ page }) => page);
3851
+ const scopeByPage = new Map(scopedPageEntries.map(({ page, scope }) => [page, scope]));
3852
+ const maxResults = typeof options.maxResults === "number" && Number.isFinite(options.maxResults) ? Math.max(1, Math.min(50, Math.floor(options.maxResults))) : 50;
3853
+ const orderedResults = [...await performDocsSearch({
3854
+ pages: toSearchSourcePages(scopedPages),
3855
+ query: options.query,
3856
+ search: {
3857
+ enabled: true,
3858
+ provider: "simple",
3859
+ maxResults: 50,
3860
+ chunking: { strategy: "section" }
3861
+ },
3862
+ locale: options.locale,
3863
+ siteTitle: options.siteTitle,
3864
+ limit: 50
3865
+ })].sort((left, right) => {
3866
+ const scoreDelta = (right.score ?? 0) - (left.score ?? 0);
3867
+ if (scoreDelta !== 0) return scoreDelta;
3868
+ if (left.url !== right.url) return left.url < right.url ? -1 : 1;
3869
+ if ((left.section ?? "") !== (right.section ?? "")) return (left.section ?? "") < (right.section ?? "") ? -1 : 1;
3870
+ if (left.id === right.id) return 0;
3871
+ return left.id < right.id ? -1 : 1;
3872
+ });
3873
+ const sectionPageUrls = new Set(orderedResults.filter((result) => result.section).map((result) => normalizeUrlPath(getDocsMcpResultPageUrl(result.url))));
3874
+ const seen = /* @__PURE__ */ new Set();
3875
+ const resolvedCandidates = orderedResults.filter((result) => {
3876
+ const pageUrl = normalizeUrlPath(getDocsMcpResultPageUrl(result.url));
3877
+ if (!result.section && sectionPageUrls.has(pageUrl)) return false;
3878
+ const key = `${pageUrl}#${getDocsMcpResultAnchor(result.url) ?? result.section ?? ""}`;
3879
+ if (seen.has(key)) return false;
3880
+ seen.add(key);
3881
+ return true;
3882
+ }).flatMap((result) => {
3883
+ const page = findDocsPage(scopedPages, getDocsMcpResultPageUrl(result.url), options.entry);
3884
+ if (!page) return [];
3885
+ const scope = scopeByPage.get(page);
3886
+ if (!scope) return [];
3887
+ const document = upsertPageAgentContractMarkdown(getDocsMcpSourceMarkdown(page), page.agent);
3888
+ const resultAnchor = getDocsMcpResultAnchor(result.url);
3889
+ const selectedSection = resultAnchor ? findDocsMarkdownSection(document, resultAnchor) : result.section ? findDocsMarkdownSection(document, result.section) : void 0;
3890
+ if ((resultAnchor || result.section) && !selectedSection) return [];
3891
+ const rawContent = (selectedSection?.content ?? document).trim();
3892
+ if (!rawContent) return [];
3893
+ return [{
3894
+ result,
3895
+ page,
3896
+ scope,
3897
+ selectedSection,
3898
+ rawContent
3899
+ }];
3900
+ }).slice(0, maxResults);
3901
+ const maxUtf8Bytes = options.tokenBudget;
3902
+ const separator = "\n\n---\n\n";
3903
+ const separatorUtf8Bytes = docsMcpUtf8Bytes(separator);
3904
+ const blocks = [];
3905
+ const sources = [];
3906
+ let usedUtf8Bytes = 0;
3907
+ for (const { result, page, scope, selectedSection, rawContent } of resolvedCandidates) {
3908
+ const anchor = selectedSection?.anchor;
3909
+ const sourceUrl = anchor ? `${page.url}#${anchor}` : page.url;
3910
+ const headerLines = [`## ${page.title}`, `Source: ${sourceUrl}`];
3911
+ if (selectedSection?.title) headerLines.push(`Section: ${selectedSection.title}`);
3912
+ if (scope.framework) headerLines.push(`Framework: ${scope.framework}`);
3913
+ if (scope.version) headerLines.push(`Version: ${scope.version}`);
3914
+ if (scope.locale) headerLines.push(`Locale: ${scope.locale}`);
3915
+ const header = headerLines.join("\n");
3916
+ const separatorBytes = blocks.length === 0 ? 0 : separatorUtf8Bytes;
3917
+ const headerBytes = docsMcpUtf8Bytes(`${header}\n\n`);
3918
+ const availableForContent = maxUtf8Bytes - usedUtf8Bytes - separatorBytes - headerBytes;
3919
+ if (availableForContent <= 0) break;
3920
+ const limited = limitDocsMcpUtf8Bytes(rawContent, availableForContent);
3921
+ if (!limited.text) break;
3922
+ const block = `${header}\n\n${limited.text}`;
3923
+ const blockUtf8Bytes = docsMcpUtf8Bytes(block);
3924
+ blocks.push(block);
3925
+ usedUtf8Bytes += separatorBytes + blockUtf8Bytes;
3926
+ sources.push({
3927
+ id: result.id,
3928
+ title: page.title,
3929
+ pageUrl: page.url,
3930
+ url: sourceUrl,
3931
+ section: selectedSection?.title,
3932
+ anchor,
3933
+ sourcePath: page.sourcePath,
3934
+ lastModified: page.lastModified,
3935
+ locale: scope.locale,
3936
+ framework: scope.framework,
3937
+ version: scope.version,
3938
+ tags: page.tags ? [...page.tags] : void 0,
3939
+ score: result.score,
3940
+ content: limited.text,
3941
+ chars: limited.text.length,
3942
+ utf8Bytes: docsMcpUtf8Bytes(limited.text),
3943
+ truncated: limited.truncated
3944
+ });
3945
+ if (limited.truncated) break;
3946
+ }
3947
+ const context = blocks.join(separator);
3948
+ usedUtf8Bytes = docsMcpUtf8Bytes(context);
3949
+ const remainingUtf8Bytes = Math.max(0, maxUtf8Bytes - usedUtf8Bytes);
3950
+ const truncated = sources.some((source) => source.truncated) || sources.length < resolvedCandidates.length;
3951
+ return {
3952
+ query: options.query,
3953
+ filters: {
3954
+ framework: options.framework,
3955
+ version: options.version,
3956
+ locale: options.locale
3957
+ },
3958
+ budget: {
3959
+ requestedTokens: options.tokenBudget,
3960
+ strategy: "utf8-bytes",
3961
+ maxUtf8Bytes,
3962
+ usedUtf8Bytes,
3963
+ conservativeTokenUpperBound: usedUtf8Bytes,
3964
+ remainingUtf8Bytes,
3965
+ truncated
3966
+ },
3967
+ resultCount: sources.length,
3968
+ candidateCount: resolvedCandidates.length,
3969
+ context,
3970
+ sources
3971
+ };
3972
+ }
2116
3973
  function findDocsPage(pages, requestedPath, entry) {
2117
3974
  const normalizedRequest = normalizeRequestedPath(requestedPath, entry);
2118
3975
  for (const page of pages) if (normalizeUrlPath(page.url) === normalizedRequest) return page;
@@ -2141,12 +3998,12 @@ function normalizeUrlPath(value) {
2141
3998
  return normalized.replace(/\/+$/, "");
2142
3999
  }
2143
4000
  function renderPageDocument(page) {
2144
- if (page.agentRawContent !== void 0) return page.agentRawContent;
4001
+ if (page.agentRawContent !== void 0) return upsertPageAgentContractMarkdown(page.agentRawContent, page.agent);
2145
4002
  const relatedLines = renderDocsRelatedMarkdownLines(page.related);
2146
4003
  const lines = [`# ${page.title}`, `URL: ${page.url}`];
2147
4004
  if (page.description) lines.push(`Description: ${page.description}`);
2148
4005
  lines.push(...relatedLines);
2149
- lines.push("", page.agentFallbackRawContent ?? page.rawContent ?? page.content);
4006
+ lines.push("", upsertPageAgentContractMarkdown(page.agentFallbackRawContent ?? page.rawContent ?? page.content, page.agent));
2150
4007
  return lines.join("\n");
2151
4008
  }
2152
4009
  function renderNavigationTree(tree) {
@@ -2175,4 +4032,4 @@ function toPageResourceUri(url) {
2175
4032
  }
2176
4033
 
2177
4034
  //#endregion
2178
- export { createDocsMcpHttpHandler, createDocsMcpServer, createFilesystemDocsMcpSource, normalizeDocsMcpRoute, resolveDocsMcpConfig, runDocsMcpStdio };
4035
+ export { DEFAULT_DOCS_MCP_CORS_ALLOWED_HEADERS, DEFAULT_DOCS_MCP_CORS_EXPOSED_HEADERS, DEFAULT_DOCS_MCP_CORS_MAX_AGE_SECONDS, DEFAULT_DOCS_MCP_MAX_BODY_BYTES, DOCS_CONFIG_SCHEMA_OPTIONS, buildDocsMcpContext, createDocsMcpHttpHandler, createDocsMcpServer, createFilesystemDocsMcpSource, getDocsConfigSchema, normalizeDocsMcpRoute, resolveDocsMcpConfig, runDocsMcpStdio };