@farming-labs/docs 0.2.50 → 0.2.53

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 (44) hide show
  1. package/dist/agent-2t4kZLGo.mjs +10 -0
  2. package/dist/{agent-CaOBIVFy.mjs → agent-BMxMpEa0.mjs} +194 -22
  3. package/dist/agent-contract-b0akaPcg.mjs +573 -0
  4. package/dist/agent-export--X9Go-wj.mjs +853 -0
  5. package/dist/{agents-DjhjZaNP.mjs → agents-CW-zTNKP.mjs} +6 -5
  6. package/dist/cli/index.mjs +43 -28
  7. package/dist/client/react.d.mts +1 -1
  8. package/dist/{cloud-ask-ai-DcMe6xOf.d.mts → cloud-ask-ai-CMzYVzVX.d.mts} +1 -1
  9. package/dist/{cloud-C_Ok9rC5.mjs → cloud-zs0qAnKJ.mjs} +3 -3
  10. package/dist/{codeblocks-z9iT794h.mjs → codeblocks-DWM4IJPL.mjs} +9 -12
  11. package/dist/{dev-Tm9Ss4D6.mjs → dev-DFeVU2Ui.mjs} +2 -2
  12. package/dist/docs-cloud-server.d.mts +2 -2
  13. package/dist/docs-cloud-server.mjs +1 -1
  14. package/dist/{doctor-O9o9kjN8.mjs → doctor-CkM5tKdD.mjs} +36 -24
  15. package/dist/{downgrade-CYEaOctn.mjs → downgrade-Dj_KXV11.mjs} +2 -2
  16. package/dist/i18n-CAlj1ADU.mjs +40 -0
  17. package/dist/index.d.mts +139 -3
  18. package/dist/index.mjs +7 -5
  19. package/dist/{init-Ch5On0zm.mjs → init-Cmz-oqnp.mjs} +3 -3
  20. package/dist/{mcp-DuCPNHS-.mjs → mcp-B4Q1PN93.mjs} +9 -5
  21. package/dist/mcp.d.mts +102 -4
  22. package/dist/mcp.mjs +1446 -66
  23. package/dist/{package-version-C8PigBZk.mjs → package-version-DxnTZH4m.mjs} +1 -1
  24. package/dist/{reading-time-CPAy1SWO.mjs → reading-time-BuK2YL23.mjs} +278 -33
  25. package/dist/{review-lm3dt7yy.mjs → review-BEsbm0l2.mjs} +38 -3
  26. package/dist/{robots-DV7u4Ire.mjs → robots-BOwkq9du.mjs} +5 -4
  27. package/dist/{robots-XVPs9lVz.mjs → robots-C6uQG-fS.mjs} +1 -1
  28. package/dist/{search-BWuU70B0.mjs → search-CJNLkdjX.mjs} +174 -79
  29. package/dist/{search-DFEhf9-s.d.mts → search-CTu6Bvq-.d.mts} +3 -1
  30. package/dist/{search-B6nEkx86.mjs → search-CtAXkk_J.mjs} +5 -4
  31. package/dist/server.d.mts +5 -5
  32. package/dist/server.mjs +8 -7
  33. package/dist/{sitemap-BEiKy4Iy.mjs → sitemap-OPb8EdNe.mjs} +6 -5
  34. package/dist/{sitemap-server-xEHtDUDB.mjs → sitemap-server-DnilMEiZ.mjs} +1 -1
  35. package/dist/{templates-uaauJcTO.mjs → templates-Bty9M6lA.mjs} +11 -5
  36. package/dist/{types-XqGLsmOD.d.mts → types-CdKu8KmO.d.mts} +118 -2
  37. package/dist/{upgrade-DzQtpgJM.mjs → upgrade-BNdBB9tQ.mjs} +2 -2
  38. package/package.json +1 -1
  39. package/dist/agent-DXDOMJBw.mjs +0 -9
  40. /package/dist/{cloud-ask-ai-Cl-xaV8j.mjs → cloud-ask-ai-BFlx8VMO.mjs} +0 -0
  41. /package/dist/{config-tiQCZ46q.mjs → config-DhIyFsy9.mjs} +0 -0
  42. /package/dist/{errors-CW1LnxaQ.mjs → errors-BxfYhp1L.mjs} +0 -0
  43. /package/dist/{review-B7goPYUb.mjs → review-BHdy75TE.mjs} +0 -0
  44. /package/dist/{utils-D-xTRNuh.mjs → utils-CMZTjRQT.mjs} +0 -0
package/dist/mcp.mjs CHANGED
@@ -1,6 +1,7 @@
1
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";
2
+ import { a as normalizePageAgentFrontmatter, i as hasStructuredPageAgentContract, l as upsertPageAgentContractMarkdown } from "./agent-contract-b0akaPcg.mjs";
3
+ import { It as normalizeDocsRelated, Lt as renderDocsRelatedMarkdownLines } from "./agent-BMxMpEa0.mjs";
4
+ 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-CJNLkdjX.mjs";
4
5
  import matter from "gray-matter";
5
6
  import fs from "node:fs";
6
7
  import path from "node:path";
@@ -14,6 +15,25 @@ 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";
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
+ ]);
17
37
  const DOCS_CONFIG_SCHEMA_OPTIONS = [
18
38
  {
19
39
  path: "entry",
@@ -328,6 +348,70 @@ const DOCS_CONFIG_SCHEMA_OPTIONS = [
328
348
  default: "0.0.0",
329
349
  description: "Version string reported to MCP clients."
330
350
  },
351
+ {
352
+ path: "mcp.security",
353
+ name: "security",
354
+ type: "DocsMcpSecurityConfig",
355
+ description: "Streamable HTTP Origin validation, optional authentication, and request-size controls. The stdio transport is unaffected.",
356
+ children: [
357
+ {
358
+ path: "mcp.security.allowedOrigins",
359
+ name: "allowedOrigins",
360
+ type: "\"same-origin\" | string[] | callback",
361
+ default: "same-origin",
362
+ 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."
363
+ },
364
+ {
365
+ path: "mcp.security.authenticate",
366
+ name: "authenticate",
367
+ type: "DocsMcpAuthenticate",
368
+ default: "public (callback omitted)",
369
+ description: "Opt-in HTTP authentication callback. Return a principal to continue, null for 401, or a Response to control the rejection."
370
+ },
371
+ {
372
+ path: "mcp.security.maxBodyBytes",
373
+ name: "maxBodyBytes",
374
+ type: "number",
375
+ default: DEFAULT_DOCS_MCP_MAX_BODY_BYTES,
376
+ description: "Maximum accepted Streamable HTTP POST body size in bytes."
377
+ },
378
+ {
379
+ path: "mcp.security.cors",
380
+ name: "cors",
381
+ type: "boolean | DocsMcpCorsConfig",
382
+ default: true,
383
+ description: "Emit exact-Origin CORS responses for Origins accepted by allowedOrigins. Use an object for credentials, additional headers, and preflight cache controls.",
384
+ children: [
385
+ {
386
+ path: "mcp.security.cors.allowedHeaders",
387
+ name: "allowedHeaders",
388
+ type: "string[]",
389
+ description: "Additional request headers accepted during browser preflight."
390
+ },
391
+ {
392
+ path: "mcp.security.cors.exposedHeaders",
393
+ name: "exposedHeaders",
394
+ type: "string[]",
395
+ description: "Additional MCP response headers exposed to browser JavaScript."
396
+ },
397
+ {
398
+ path: "mcp.security.cors.allowCredentials",
399
+ name: "allowCredentials",
400
+ type: "boolean",
401
+ default: false,
402
+ description: "Allow credentialed browser requests using the validated exact Origin. Wildcard credentials are never emitted."
403
+ },
404
+ {
405
+ path: "mcp.security.cors.maxAgeSeconds",
406
+ name: "maxAgeSeconds",
407
+ type: "number",
408
+ default: DEFAULT_DOCS_MCP_CORS_MAX_AGE_SECONDS,
409
+ description: "Browser preflight cache lifetime in seconds."
410
+ }
411
+ ]
412
+ }
413
+ ]
414
+ },
331
415
  {
332
416
  path: "mcp.tools",
333
417
  name: "tools",
@@ -349,6 +433,20 @@ const DOCS_CONFIG_SCHEMA_OPTIONS = [
349
433
  default: true,
350
434
  description: "Expose the list_pages tool."
351
435
  },
436
+ {
437
+ path: "mcp.tools.listTasks",
438
+ name: "listTasks",
439
+ type: "boolean",
440
+ default: true,
441
+ description: "Expose the list_tasks tool."
442
+ },
443
+ {
444
+ path: "mcp.tools.readTask",
445
+ name: "readTask",
446
+ type: "boolean",
447
+ default: true,
448
+ description: "Expose the read_task tool."
449
+ },
352
450
  {
353
451
  path: "mcp.tools.getNavigation",
354
452
  name: "getNavigation",
@@ -383,6 +481,13 @@ const DOCS_CONFIG_SCHEMA_OPTIONS = [
383
481
  type: "boolean",
384
482
  default: true,
385
483
  description: "Expose the get_config_schema tool."
484
+ },
485
+ {
486
+ path: "mcp.tools.getContext",
487
+ name: "getContext",
488
+ type: "boolean",
489
+ default: true,
490
+ description: "Expose deterministic get_context retrieval with a conservative UTF-8 byte ceiling."
386
491
  }
387
492
  ]
388
493
  }
@@ -492,6 +597,19 @@ export default defineDocs({
492
597
  getCodeExamples: true,
493
598
  },
494
599
  },
600
+ });`
601
+ },
602
+ {
603
+ title: "Opt-in MCP authentication",
604
+ code: `export default defineDocs({
605
+ mcp: {
606
+ security: {
607
+ async authenticate({ request }) {
608
+ const user = await authenticateRequest(request);
609
+ return user ? { id: user.id, scopes: ["docs:read"] } : null;
610
+ },
611
+ },
612
+ },
495
613
  });`
496
614
  },
497
615
  {
@@ -524,8 +642,74 @@ const searchDocsInputSchema = z.object({
524
642
  });
525
643
  const readPageInputSchema = z.object({
526
644
  path: z.string().min(1),
645
+ locale: z.string().min(1).optional(),
646
+ section: z.string().trim().min(1).optional(),
647
+ maxChars: z.number().int().min(256).max(1e6).optional()
648
+ });
649
+ const listTasksInputSchema = z.object({
650
+ query: z.string().trim().min(1).optional(),
651
+ framework: z.string().trim().min(1).optional(),
652
+ version: z.string().trim().min(1).optional(),
653
+ package: z.string().trim().min(1).optional(),
527
654
  locale: z.string().min(1).optional()
528
655
  });
656
+ const readTaskInputSchema = readPageInputSchema;
657
+ const pageAgentAppliesToOutputSchema = z.object({
658
+ framework: z.array(z.string()).optional(),
659
+ version: z.array(z.string()).optional(),
660
+ package: z.array(z.string()).optional()
661
+ });
662
+ const pageAgentCommandOutputSchema = z.union([z.string(), z.object({
663
+ run: z.string(),
664
+ cwd: z.string().optional(),
665
+ description: z.string().optional()
666
+ })]);
667
+ const pageAgentVerificationOutputSchema = z.union([z.string(), z.object({
668
+ description: z.string().optional(),
669
+ run: z.string().optional(),
670
+ expect: z.string().optional()
671
+ })]);
672
+ const pageAgentFailureModeOutputSchema = z.union([z.string(), z.object({
673
+ symptom: z.string(),
674
+ resolution: z.string().optional()
675
+ })]);
676
+ const pageAgentContractOutputSchema = z.object({
677
+ tokenBudget: z.number().optional(),
678
+ task: z.string().optional(),
679
+ outcome: z.string().optional(),
680
+ appliesTo: pageAgentAppliesToOutputSchema.optional(),
681
+ prerequisites: z.array(z.string()).optional(),
682
+ files: z.array(z.string()).optional(),
683
+ commands: z.array(pageAgentCommandOutputSchema).optional(),
684
+ sideEffects: z.array(z.string()).optional(),
685
+ verification: z.array(pageAgentVerificationOutputSchema).optional(),
686
+ rollback: z.array(z.string()).optional(),
687
+ failureModes: z.array(pageAgentFailureModeOutputSchema).optional()
688
+ });
689
+ const taskSummaryOutputSchema = z.object({
690
+ slug: z.string(),
691
+ url: z.string(),
692
+ title: z.string(),
693
+ description: z.string().optional(),
694
+ task: z.string().optional(),
695
+ outcome: z.string().optional(),
696
+ appliesTo: pageAgentAppliesToOutputSchema.optional()
697
+ });
698
+ const listTasksOutputSchema = z.object({
699
+ resultCount: z.number().int().nonnegative(),
700
+ tasks: z.array(taskSummaryOutputSchema)
701
+ });
702
+ const readTaskOutputSchema = z.object({
703
+ page: z.object({
704
+ slug: z.string(),
705
+ url: z.string(),
706
+ title: z.string(),
707
+ description: z.string().optional(),
708
+ sourcePath: z.string().optional(),
709
+ lastModified: z.string().optional()
710
+ }),
711
+ contract: pageAgentContractOutputSchema
712
+ });
529
713
  const listPagesInputSchema = z.object({ locale: z.string().min(1).optional() });
530
714
  const listDocsInputSchema = z.object({
531
715
  section: z.string().trim().min(1).optional(),
@@ -546,6 +730,195 @@ const getCodeExamplesInputSchema = z.object({
546
730
  limit: z.number().int().min(1).max(50).optional(),
547
731
  locale: z.string().min(1).optional()
548
732
  });
733
+ const getContextInputSchema = z.object({
734
+ query: z.string().trim().min(1),
735
+ framework: z.string().trim().min(1).optional(),
736
+ version: z.string().trim().min(1).optional(),
737
+ locale: z.string().trim().min(1).optional(),
738
+ tokenBudget: z.number().int().min(MIN_MCP_CONTEXT_TOKEN_BUDGET).max(MAX_MCP_CONTEXT_TOKEN_BUDGET).default(DEFAULT_MCP_CONTEXT_TOKEN_BUDGET)
739
+ });
740
+ const relatedLinkOutputSchema = z.object({ href: z.string() });
741
+ const pageAgentContractSummaryOutputSchema = z.object({
742
+ hasContract: z.boolean(),
743
+ task: z.string().optional(),
744
+ outcome: z.string().optional(),
745
+ appliesTo: pageAgentAppliesToOutputSchema.optional()
746
+ });
747
+ const pageSummaryOutputSchema = z.object({
748
+ slug: z.string(),
749
+ url: z.string(),
750
+ title: z.string(),
751
+ description: z.string().optional(),
752
+ agent: pageAgentContractSummaryOutputSchema.optional(),
753
+ icon: z.string().optional(),
754
+ sourcePath: z.string().optional(),
755
+ lastModified: z.string().optional()
756
+ });
757
+ const docsSectionOutputSchema = z.lazy(() => z.object({
758
+ slug: z.string(),
759
+ title: z.string(),
760
+ url: z.string().optional(),
761
+ description: z.string().optional(),
762
+ icon: z.string().optional(),
763
+ pageCount: z.number().int().nonnegative(),
764
+ pages: z.array(pageSummaryOutputSchema),
765
+ sections: z.array(docsSectionOutputSchema)
766
+ }));
767
+ const listPagesOutputSchema = z.object({ pages: z.array(pageSummaryOutputSchema) });
768
+ const listDocsOutputSchema = z.object({
769
+ section: z.string().optional(),
770
+ resultCount: z.number().int().nonnegative(),
771
+ sectionCount: z.number().int().nonnegative(),
772
+ pages: z.array(pageSummaryOutputSchema),
773
+ rootPages: z.array(pageSummaryOutputSchema),
774
+ sections: z.array(docsSectionOutputSchema)
775
+ });
776
+ const navigationPageOutputSchema = z.object({
777
+ type: z.literal("page"),
778
+ name: z.string(),
779
+ url: z.string(),
780
+ icon: z.string().optional(),
781
+ description: z.string().optional()
782
+ });
783
+ const navigationNodeOutputSchema = z.lazy(() => z.discriminatedUnion("type", [navigationPageOutputSchema, z.object({
784
+ type: z.literal("folder"),
785
+ name: z.string(),
786
+ icon: z.string().optional(),
787
+ index: navigationPageOutputSchema.optional(),
788
+ children: z.array(navigationNodeOutputSchema)
789
+ })]));
790
+ const navigationOutputSchema = z.object({
791
+ navigation: z.object({
792
+ name: z.string(),
793
+ children: z.array(navigationNodeOutputSchema)
794
+ }),
795
+ markdown: z.string()
796
+ });
797
+ const searchResultOutputSchema = z.object({
798
+ id: z.string(),
799
+ url: z.string(),
800
+ content: z.string(),
801
+ description: z.string().optional(),
802
+ type: z.enum([
803
+ "page",
804
+ "heading",
805
+ "text"
806
+ ]),
807
+ score: z.number().optional(),
808
+ section: z.string().optional()
809
+ });
810
+ const searchDocsOutputSchema = z.object({ results: z.array(searchResultOutputSchema) });
811
+ const codeExampleOutputSchema = z.object({
812
+ id: z.string(),
813
+ page: z.object({
814
+ slug: z.string(),
815
+ url: z.string(),
816
+ title: z.string(),
817
+ description: z.string().optional(),
818
+ sourcePath: z.string().optional(),
819
+ lastModified: z.string().optional()
820
+ }),
821
+ language: z.string().optional(),
822
+ title: z.string().optional(),
823
+ framework: z.string().optional(),
824
+ packageManager: z.string().optional(),
825
+ runnable: z.boolean(),
826
+ meta: z.record(z.string(), z.union([z.string(), z.boolean()])),
827
+ code: z.string()
828
+ });
829
+ const codeExamplesOutputSchema = z.object({ examples: z.array(codeExampleOutputSchema) });
830
+ const configSchemaOptionOutputSchema = z.lazy(() => z.object({
831
+ path: z.string(),
832
+ name: z.string(),
833
+ type: z.string(),
834
+ default: z.union([
835
+ z.string(),
836
+ z.boolean(),
837
+ z.number(),
838
+ z.null()
839
+ ]).optional(),
840
+ description: z.string(),
841
+ docs: z.string().optional(),
842
+ values: z.array(z.string()).optional(),
843
+ children: z.array(configSchemaOptionOutputSchema).optional()
844
+ }));
845
+ const configSchemaOutputSchema = z.object({
846
+ schemaVersion: z.literal(1),
847
+ configFile: z.literal("docs.config.ts"),
848
+ description: z.string(),
849
+ filters: z.object({
850
+ option: z.string().optional(),
851
+ query: z.string().optional()
852
+ }).optional(),
853
+ resultCount: z.number().int().nonnegative(),
854
+ options: z.array(configSchemaOptionOutputSchema),
855
+ examples: z.array(z.object({
856
+ title: z.string(),
857
+ code: z.string()
858
+ }))
859
+ });
860
+ const readPageOutputSchema = z.object({
861
+ page: z.object({
862
+ slug: z.string(),
863
+ url: z.string(),
864
+ title: z.string(),
865
+ description: z.string().optional(),
866
+ related: z.array(relatedLinkOutputSchema).optional(),
867
+ icon: z.string().optional(),
868
+ sourcePath: z.string().optional(),
869
+ lastModified: z.string().optional(),
870
+ locale: z.string().optional(),
871
+ framework: z.string().optional(),
872
+ version: z.string().optional(),
873
+ tags: z.array(z.string()).optional()
874
+ }),
875
+ document: z.string(),
876
+ section: z.string().optional(),
877
+ anchor: z.string().optional(),
878
+ chars: z.number().int().nonnegative(),
879
+ totalChars: z.number().int().nonnegative(),
880
+ truncated: z.boolean()
881
+ });
882
+ const contextSourceOutputSchema = z.object({
883
+ id: z.string(),
884
+ title: z.string(),
885
+ pageUrl: z.string(),
886
+ url: z.string(),
887
+ section: z.string().optional(),
888
+ anchor: z.string().optional(),
889
+ sourcePath: z.string().optional(),
890
+ lastModified: z.string().optional(),
891
+ locale: z.string().optional(),
892
+ framework: z.string().optional(),
893
+ version: z.string().optional(),
894
+ tags: z.array(z.string()).optional(),
895
+ score: z.number().optional(),
896
+ content: z.string(),
897
+ chars: z.number().int().nonnegative(),
898
+ utf8Bytes: z.number().int().nonnegative(),
899
+ truncated: z.boolean()
900
+ });
901
+ const contextOutputSchema = z.object({
902
+ query: z.string(),
903
+ filters: z.object({
904
+ framework: z.string().optional(),
905
+ version: z.string().optional(),
906
+ locale: z.string().optional()
907
+ }),
908
+ budget: z.object({
909
+ requestedTokens: z.number().int().positive(),
910
+ strategy: z.literal("utf8-bytes"),
911
+ maxUtf8Bytes: z.number().int().positive(),
912
+ usedUtf8Bytes: z.number().int().nonnegative(),
913
+ conservativeTokenUpperBound: z.number().int().nonnegative(),
914
+ remainingUtf8Bytes: z.number().int().nonnegative(),
915
+ truncated: z.boolean()
916
+ }),
917
+ resultCount: z.number().int().nonnegative(),
918
+ candidateCount: z.number().int().nonnegative(),
919
+ context: z.string(),
920
+ sources: z.array(contextSourceOutputSchema)
921
+ });
549
922
  function normalizeDocsMcpRoute(route) {
550
923
  if (!route || route.trim().length === 0) return DEFAULT_MCP_ROUTE;
551
924
  const normalized = `/${route}`.replace(/\/+/g, "/");
@@ -561,11 +934,15 @@ function resolveDocsMcpConfig(mcp, defaults = {}) {
561
934
  listDocs: true,
562
935
  listPages: true,
563
936
  readPage: true,
937
+ listTasks: true,
938
+ readTask: true,
564
939
  searchDocs: true,
565
940
  getNavigation: true,
566
941
  getCodeExamples: true,
567
- getConfigSchema: true
568
- }
942
+ getConfigSchema: true,
943
+ getContext: true
944
+ },
945
+ security: resolveDocsMcpSecurityConfig()
569
946
  };
570
947
  const config = mcp && typeof mcp === "object" ? mcp : {};
571
948
  return {
@@ -577,13 +954,49 @@ function resolveDocsMcpConfig(mcp, defaults = {}) {
577
954
  listDocs: config.tools?.listDocs ?? true,
578
955
  listPages: config.tools?.listPages ?? true,
579
956
  readPage: config.tools?.readPage ?? true,
957
+ listTasks: config.tools?.listTasks ?? true,
958
+ readTask: config.tools?.readTask ?? true,
580
959
  searchDocs: config.tools?.searchDocs ?? true,
581
960
  getNavigation: config.tools?.getNavigation ?? true,
582
961
  getCodeExamples: config.tools?.getCodeExamples ?? true,
583
- getConfigSchema: config.tools?.getConfigSchema ?? true
584
- }
962
+ getConfigSchema: config.tools?.getConfigSchema ?? true,
963
+ getContext: config.tools?.getContext ?? true
964
+ },
965
+ security: resolveDocsMcpSecurityConfig(config.security)
585
966
  };
586
967
  }
968
+ function resolveDocsMcpSecurityConfig(security) {
969
+ const configuredMaxBodyBytes = security?.maxBodyBytes;
970
+ const maxBodyBytes = typeof configuredMaxBodyBytes === "number" && Number.isFinite(configuredMaxBodyBytes) && configuredMaxBodyBytes > 0 ? Math.floor(configuredMaxBodyBytes) : DEFAULT_DOCS_MCP_MAX_BODY_BYTES;
971
+ return {
972
+ allowedOrigins: security?.allowedOrigins ?? "same-origin",
973
+ authenticate: security?.authenticate,
974
+ maxBodyBytes,
975
+ cors: resolveDocsMcpCorsConfig(security?.cors)
976
+ };
977
+ }
978
+ function resolveDocsMcpCorsConfig(cors) {
979
+ const config = cors && typeof cors === "object" ? cors : {};
980
+ const configuredMaxAge = config.maxAgeSeconds;
981
+ const maxAgeSeconds = typeof configuredMaxAge === "number" && Number.isFinite(configuredMaxAge) && configuredMaxAge >= 0 ? Math.floor(configuredMaxAge) : DEFAULT_DOCS_MCP_CORS_MAX_AGE_SECONDS;
982
+ return {
983
+ enabled: cors !== false,
984
+ allowedHeaders: mergeHttpHeaderNames(DEFAULT_DOCS_MCP_CORS_ALLOWED_HEADERS, config.allowedHeaders),
985
+ exposedHeaders: mergeHttpHeaderNames(DEFAULT_DOCS_MCP_CORS_EXPOSED_HEADERS, config.exposedHeaders),
986
+ allowCredentials: config.allowCredentials === true,
987
+ maxAgeSeconds
988
+ };
989
+ }
990
+ function mergeHttpHeaderNames(defaults, configured) {
991
+ const headers = /* @__PURE__ */ new Map();
992
+ for (const header of [...defaults, ...configured ?? []]) {
993
+ const normalized = header.trim();
994
+ if (!/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(normalized)) continue;
995
+ const key = normalized.toLowerCase();
996
+ if (!headers.has(key)) headers.set(key, normalized);
997
+ }
998
+ return [...headers.values()];
999
+ }
587
1000
  function createFilesystemDocsMcpSource(options = {}) {
588
1001
  const rootDir = options.rootDir ?? process.cwd();
589
1002
  const entry = normalizePathSegment(options.entry ?? "docs") || "docs";
@@ -618,6 +1031,15 @@ function nowMs() {
618
1031
  function durationMs(startedAt) {
619
1032
  return Math.max(0, Date.now() - startedAt);
620
1033
  }
1034
+ function createStructuredTextResult(structuredContent, text) {
1035
+ return {
1036
+ content: [{
1037
+ type: "text",
1038
+ text: text ?? JSON.stringify(structuredContent, null, 2)
1039
+ }],
1040
+ structuredContent
1041
+ };
1042
+ }
621
1043
  async function createDocsMcpServer(options) {
622
1044
  const resolved = resolveDocsMcpConfig(options.mcp, {
623
1045
  defaultName: options.defaultName ?? options.source.siteTitle ?? DEFAULT_MCP_NAME,
@@ -634,6 +1056,12 @@ async function createDocsMcpServer(options) {
634
1056
  search: options.search
635
1057
  };
636
1058
  const telemetryFramework = options.telemetryFramework ?? "mcp";
1059
+ function getSourcePages(locale) {
1060
+ return options.source.getPages(locale, options.requestContext);
1061
+ }
1062
+ function getSourceNavigation(locale) {
1063
+ return options.source.getNavigation(locale, options.requestContext);
1064
+ }
637
1065
  function trackMcpTool(tool, values) {
638
1066
  emitDocsTelemetryMcpToolEvent(telemetryConfig, {
639
1067
  framework: telemetryFramework,
@@ -642,8 +1070,8 @@ async function createDocsMcpServer(options) {
642
1070
  resultCount: values?.resultCount
643
1071
  });
644
1072
  }
645
- const defaultPages = dedupePages(await options.source.getPages());
646
- const defaultTree = await options.source.getNavigation();
1073
+ const defaultPages = dedupePages(await getSourcePages());
1074
+ const defaultTree = await getSourceNavigation();
647
1075
  server.registerResource("docs-navigation", "docs://navigation", {
648
1076
  title: "Docs Navigation",
649
1077
  description: "Structured navigation tree for the documentation site.",
@@ -669,6 +1097,7 @@ async function createDocsMcpServer(options) {
669
1097
  title: "List docs pages",
670
1098
  description: "List the known documentation pages with titles, slugs, and URLs.",
671
1099
  inputSchema: listPagesInputSchema,
1100
+ outputSchema: listPagesOutputSchema,
672
1101
  annotations: { readOnlyHint: true }
673
1102
  }, async ({ locale }) => {
674
1103
  const startedAt = nowMs();
@@ -687,7 +1116,7 @@ async function createDocsMcpServer(options) {
687
1116
  metadata: { tool: "list_pages" }
688
1117
  });
689
1118
  try {
690
- const pages = toPageSummaries(dedupePages(await options.source.getPages(locale)));
1119
+ const pages = toPageSummaries(dedupePages(await getSourcePages(locale)));
691
1120
  const elapsed = durationMs(startedAt);
692
1121
  await emitDocsAnalyticsEvent(options.analytics, {
693
1122
  type: "mcp_tool",
@@ -717,10 +1146,7 @@ async function createDocsMcpServer(options) {
717
1146
  outputPreview: { resultCount: pages.length },
718
1147
  metadata: { tool: "list_pages" }
719
1148
  });
720
- return { content: [{
721
- type: "text",
722
- text: JSON.stringify({ pages }, null, 2)
723
- }] };
1149
+ return createStructuredTextResult({ pages });
724
1150
  } catch (error) {
725
1151
  const elapsed = durationMs(startedAt);
726
1152
  await emitDocsAgentTraceEvent(options.observability, {
@@ -744,6 +1170,7 @@ async function createDocsMcpServer(options) {
744
1170
  title: "List docs by section",
745
1171
  description: "List documentation pages grouped by section, optionally narrowed to one section.",
746
1172
  inputSchema: listDocsInputSchema,
1173
+ outputSchema: listDocsOutputSchema,
747
1174
  annotations: { readOnlyHint: true }
748
1175
  }, async ({ section, locale }) => {
749
1176
  const startedAt = nowMs();
@@ -765,7 +1192,7 @@ async function createDocsMcpServer(options) {
765
1192
  metadata: { tool: "list_docs" }
766
1193
  });
767
1194
  try {
768
- const docs = listDocsBySection(dedupePages(await options.source.getPages(locale)), {
1195
+ const docs = listDocsBySection(dedupePages(await getSourcePages(locale)), {
769
1196
  section,
770
1197
  entry: options.source.entry
771
1198
  });
@@ -803,10 +1230,7 @@ async function createDocsMcpServer(options) {
803
1230
  },
804
1231
  metadata: { tool: "list_docs" }
805
1232
  });
806
- return { content: [{
807
- type: "text",
808
- text: JSON.stringify(docs, null, 2)
809
- }] };
1233
+ return createStructuredTextResult(docs);
810
1234
  } catch (error) {
811
1235
  const elapsed = durationMs(startedAt);
812
1236
  await emitDocsAgentTraceEvent(options.observability, {
@@ -826,10 +1250,257 @@ async function createDocsMcpServer(options) {
826
1250
  throw error;
827
1251
  }
828
1252
  });
1253
+ if (resolved.tools.listTasks) server.registerTool("list_tasks", {
1254
+ title: "List documented tasks",
1255
+ description: "List pages with actionable agent contracts, optionally filtered by text or applicability.",
1256
+ inputSchema: listTasksInputSchema,
1257
+ outputSchema: listTasksOutputSchema,
1258
+ annotations: { readOnlyHint: true }
1259
+ }, async ({ query, framework, version, package: packageName, locale }) => {
1260
+ const startedAt = nowMs();
1261
+ const trace = createDocsAgentTraceContext("mcp.tool.list_tasks");
1262
+ const callSpanId = createDocsAgentTraceId("span");
1263
+ await emitDocsAgentTraceEvent(options.observability, {
1264
+ type: "tool.call",
1265
+ source: "mcp",
1266
+ traceId: trace.traceId,
1267
+ spanId: callSpanId,
1268
+ name: "list_tasks",
1269
+ startedAt: trace.startedAt,
1270
+ status: "started",
1271
+ locale,
1272
+ inputPreview: {
1273
+ queryLength: query?.length,
1274
+ framework,
1275
+ version,
1276
+ package: packageName
1277
+ },
1278
+ metadata: { tool: "list_tasks" }
1279
+ });
1280
+ try {
1281
+ const tasks = listDocsTasks(dedupePages(await getSourcePages(locale)), {
1282
+ query,
1283
+ framework,
1284
+ version,
1285
+ package: packageName
1286
+ });
1287
+ const result = {
1288
+ resultCount: tasks.length,
1289
+ tasks
1290
+ };
1291
+ const elapsed = durationMs(startedAt);
1292
+ await emitDocsAnalyticsEvent(options.analytics, {
1293
+ type: "mcp_tool",
1294
+ source: "mcp",
1295
+ locale,
1296
+ input: query ? { query } : void 0,
1297
+ properties: {
1298
+ tool: "list_tasks",
1299
+ framework,
1300
+ version,
1301
+ package: packageName,
1302
+ resultCount: tasks.length,
1303
+ durationMs: elapsed
1304
+ }
1305
+ });
1306
+ trackMcpTool("list_tasks", {
1307
+ locale,
1308
+ resultCount: tasks.length
1309
+ });
1310
+ await emitDocsAgentTraceEvent(options.observability, {
1311
+ type: "tool.result",
1312
+ source: "mcp",
1313
+ traceId: trace.traceId,
1314
+ parentSpanId: callSpanId,
1315
+ name: "list_tasks",
1316
+ startedAt: trace.startedAt,
1317
+ endedAt: (/* @__PURE__ */ new Date()).toISOString(),
1318
+ durationMs: elapsed,
1319
+ status: "success",
1320
+ locale,
1321
+ outputPreview: { resultCount: tasks.length },
1322
+ metadata: { tool: "list_tasks" }
1323
+ });
1324
+ return {
1325
+ structuredContent: result,
1326
+ content: [{
1327
+ type: "text",
1328
+ text: JSON.stringify(result, null, 2)
1329
+ }]
1330
+ };
1331
+ } catch (error) {
1332
+ const elapsed = durationMs(startedAt);
1333
+ await emitDocsAgentTraceEvent(options.observability, {
1334
+ type: "tool.error",
1335
+ source: "mcp",
1336
+ traceId: trace.traceId,
1337
+ parentSpanId: callSpanId,
1338
+ name: "list_tasks",
1339
+ startedAt: trace.startedAt,
1340
+ endedAt: (/* @__PURE__ */ new Date()).toISOString(),
1341
+ durationMs: elapsed,
1342
+ status: "error",
1343
+ locale,
1344
+ outputPreview: { message: error instanceof Error ? error.message : "Unknown error" },
1345
+ metadata: { tool: "list_tasks" }
1346
+ });
1347
+ throw error;
1348
+ }
1349
+ });
1350
+ if (resolved.tools.readTask) server.registerTool("read_task", {
1351
+ title: "Read a documented task",
1352
+ description: "Read the full structured agent contract for a page by slug or URL path.",
1353
+ inputSchema: readTaskInputSchema,
1354
+ outputSchema: readTaskOutputSchema,
1355
+ annotations: { readOnlyHint: true }
1356
+ }, async ({ path: requestedPath, locale }) => {
1357
+ const startedAt = nowMs();
1358
+ const trace = createDocsAgentTraceContext("mcp.tool.read_task");
1359
+ const callSpanId = createDocsAgentTraceId("span");
1360
+ await emitDocsAgentTraceEvent(options.observability, {
1361
+ type: "tool.call",
1362
+ source: "mcp",
1363
+ traceId: trace.traceId,
1364
+ spanId: callSpanId,
1365
+ name: "read_task",
1366
+ startedAt: trace.startedAt,
1367
+ status: "started",
1368
+ locale,
1369
+ inputPreview: {
1370
+ path: requestedPath,
1371
+ locale
1372
+ },
1373
+ metadata: { tool: "read_task" }
1374
+ });
1375
+ try {
1376
+ const page = findDocsPage(dedupePages(await getSourcePages(locale)), requestedPath, options.source.entry);
1377
+ const contract = normalizePageAgentFrontmatter(page?.agent);
1378
+ if (!page || !contract || !hasStructuredPageAgentContract(contract)) {
1379
+ const elapsed = durationMs(startedAt);
1380
+ const reason = page ? "contract_not_found" : "page_not_found";
1381
+ const errorResult = { error: page ? `The docs page matched "${requestedPath}", but it has no actionable agent contract.` : `No docs page matched "${requestedPath}".` };
1382
+ await emitDocsAnalyticsEvent(options.analytics, {
1383
+ type: "mcp_tool",
1384
+ source: "mcp",
1385
+ locale,
1386
+ properties: {
1387
+ tool: "read_task",
1388
+ path: requestedPath,
1389
+ found: false,
1390
+ reason,
1391
+ durationMs: elapsed
1392
+ }
1393
+ });
1394
+ trackMcpTool("read_task", {
1395
+ locale,
1396
+ resultCount: 0
1397
+ });
1398
+ await emitDocsAgentTraceEvent(options.observability, {
1399
+ type: "tool.error",
1400
+ source: "mcp",
1401
+ traceId: trace.traceId,
1402
+ parentSpanId: callSpanId,
1403
+ name: "read_task",
1404
+ startedAt: trace.startedAt,
1405
+ endedAt: (/* @__PURE__ */ new Date()).toISOString(),
1406
+ durationMs: elapsed,
1407
+ status: "error",
1408
+ locale,
1409
+ outputPreview: {
1410
+ found: false,
1411
+ path: requestedPath
1412
+ },
1413
+ metadata: {
1414
+ tool: "read_task",
1415
+ reason
1416
+ }
1417
+ });
1418
+ return {
1419
+ content: [{
1420
+ type: "text",
1421
+ text: JSON.stringify(errorResult, null, 2)
1422
+ }],
1423
+ isError: true
1424
+ };
1425
+ }
1426
+ const result = {
1427
+ page: {
1428
+ slug: page.slug,
1429
+ url: page.url,
1430
+ title: page.title,
1431
+ ...page.description ? { description: page.description } : {},
1432
+ ...page.sourcePath ? { sourcePath: page.sourcePath } : {},
1433
+ ...page.lastModified ? { lastModified: page.lastModified } : {}
1434
+ },
1435
+ contract
1436
+ };
1437
+ const elapsed = durationMs(startedAt);
1438
+ await emitDocsAnalyticsEvent(options.analytics, {
1439
+ type: "mcp_tool",
1440
+ source: "mcp",
1441
+ locale,
1442
+ path: page.url,
1443
+ properties: {
1444
+ tool: "read_task",
1445
+ requestedPath,
1446
+ slug: page.slug,
1447
+ found: true,
1448
+ durationMs: elapsed
1449
+ }
1450
+ });
1451
+ trackMcpTool("read_task", {
1452
+ locale,
1453
+ resultCount: 1
1454
+ });
1455
+ await emitDocsAgentTraceEvent(options.observability, {
1456
+ type: "tool.result",
1457
+ source: "mcp",
1458
+ traceId: trace.traceId,
1459
+ parentSpanId: callSpanId,
1460
+ name: "read_task",
1461
+ startedAt: trace.startedAt,
1462
+ endedAt: (/* @__PURE__ */ new Date()).toISOString(),
1463
+ durationMs: elapsed,
1464
+ status: "success",
1465
+ locale,
1466
+ path: page.url,
1467
+ outputPreview: {
1468
+ found: true,
1469
+ slug: page.slug
1470
+ },
1471
+ metadata: { tool: "read_task" }
1472
+ });
1473
+ return {
1474
+ structuredContent: result,
1475
+ content: [{
1476
+ type: "text",
1477
+ text: JSON.stringify(result, null, 2)
1478
+ }]
1479
+ };
1480
+ } catch (error) {
1481
+ const elapsed = durationMs(startedAt);
1482
+ await emitDocsAgentTraceEvent(options.observability, {
1483
+ type: "tool.error",
1484
+ source: "mcp",
1485
+ traceId: trace.traceId,
1486
+ parentSpanId: callSpanId,
1487
+ name: "read_task",
1488
+ startedAt: trace.startedAt,
1489
+ endedAt: (/* @__PURE__ */ new Date()).toISOString(),
1490
+ durationMs: elapsed,
1491
+ status: "error",
1492
+ locale,
1493
+ outputPreview: { message: error instanceof Error ? error.message : "Unknown error" },
1494
+ metadata: { tool: "read_task" }
1495
+ });
1496
+ throw error;
1497
+ }
1498
+ });
829
1499
  if (resolved.tools.getNavigation) server.registerTool("get_navigation", {
830
1500
  title: "Get docs navigation",
831
1501
  description: "Return the documentation navigation tree for the current docs site.",
832
1502
  inputSchema: getNavigationInputSchema,
1503
+ outputSchema: navigationOutputSchema,
833
1504
  annotations: { readOnlyHint: true }
834
1505
  }, async ({ locale }) => {
835
1506
  const startedAt = nowMs();
@@ -848,7 +1519,8 @@ async function createDocsMcpServer(options) {
848
1519
  metadata: { tool: "get_navigation" }
849
1520
  });
850
1521
  try {
851
- const text = renderNavigationTree(await options.source.getNavigation(locale));
1522
+ const tree = await getSourceNavigation(locale);
1523
+ const text = renderNavigationTree(tree);
852
1524
  const elapsed = durationMs(startedAt);
853
1525
  await emitDocsAnalyticsEvent(options.analytics, {
854
1526
  type: "mcp_tool",
@@ -874,10 +1546,10 @@ async function createDocsMcpServer(options) {
874
1546
  outputPreview: { chars: text.length },
875
1547
  metadata: { tool: "get_navigation" }
876
1548
  });
877
- return { content: [{
878
- type: "text",
879
- text
880
- }] };
1549
+ return createStructuredTextResult({
1550
+ navigation: tree,
1551
+ markdown: text
1552
+ }, text);
881
1553
  } catch (error) {
882
1554
  const elapsed = durationMs(startedAt);
883
1555
  await emitDocsAgentTraceEvent(options.observability, {
@@ -901,6 +1573,7 @@ async function createDocsMcpServer(options) {
901
1573
  title: "Get docs config schema",
902
1574
  description: "Return structured docs.config.ts option metadata, optionally filtered by option path or query.",
903
1575
  inputSchema: getConfigSchemaInputSchema,
1576
+ outputSchema: configSchemaOutputSchema,
904
1577
  annotations: { readOnlyHint: true }
905
1578
  }, async ({ option, query }) => {
906
1579
  const startedAt = nowMs();
@@ -952,10 +1625,7 @@ async function createDocsMcpServer(options) {
952
1625
  outputPreview: { resultCount: schema.resultCount },
953
1626
  metadata: { tool: "get_config_schema" }
954
1627
  });
955
- return { content: [{
956
- type: "text",
957
- text: JSON.stringify(schema, null, 2)
958
- }] };
1628
+ return createStructuredTextResult(schema);
959
1629
  } catch (error) {
960
1630
  const elapsed = durationMs(startedAt);
961
1631
  await emitDocsAgentTraceEvent(options.observability, {
@@ -978,6 +1648,7 @@ async function createDocsMcpServer(options) {
978
1648
  title: "Search documentation",
979
1649
  description: "Search the docs by keyword across titles, descriptions, and page content.",
980
1650
  inputSchema: searchDocsInputSchema,
1651
+ outputSchema: searchDocsOutputSchema,
981
1652
  annotations: { readOnlyHint: true }
982
1653
  }, async ({ query, limit, locale }) => {
983
1654
  const startedAt = nowMs();
@@ -1002,7 +1673,7 @@ async function createDocsMcpServer(options) {
1002
1673
  });
1003
1674
  try {
1004
1675
  const results = await performDocsSearch({
1005
- pages: toSearchSourcePages(dedupePages(await options.source.getPages(locale))),
1676
+ pages: toSearchSourcePages(dedupePages(await getSourcePages(locale))),
1006
1677
  query,
1007
1678
  search: toolSearchConfig ?? true,
1008
1679
  locale,
@@ -1041,10 +1712,7 @@ async function createDocsMcpServer(options) {
1041
1712
  outputPreview: { resultCount: results.length },
1042
1713
  metadata: { tool: "search_docs" }
1043
1714
  });
1044
- return { content: [{
1045
- type: "text",
1046
- text: JSON.stringify({ results }, null, 2)
1047
- }] };
1715
+ return createStructuredTextResult({ results });
1048
1716
  } catch (error) {
1049
1717
  const elapsed = durationMs(startedAt);
1050
1718
  await emitDocsAgentTraceEvent(options.observability, {
@@ -1068,6 +1736,7 @@ async function createDocsMcpServer(options) {
1068
1736
  title: "Get docs code examples",
1069
1737
  description: "Return fenced code examples from the docs with parsed metadata such as title, framework, packageManager, and runnable.",
1070
1738
  inputSchema: getCodeExamplesInputSchema,
1739
+ outputSchema: codeExamplesOutputSchema,
1071
1740
  annotations: { readOnlyHint: true }
1072
1741
  }, async ({ query, path: requestedPath, framework, packageManager, language, runnable, limit, locale }) => {
1073
1742
  const startedAt = nowMs();
@@ -1095,7 +1764,7 @@ async function createDocsMcpServer(options) {
1095
1764
  metadata: { tool: "get_code_examples" }
1096
1765
  });
1097
1766
  try {
1098
- const pages = dedupePages(await options.source.getPages(locale));
1767
+ const pages = dedupePages(await getSourcePages(locale));
1099
1768
  const matchedPage = requestedPath ? findDocsPage(pages, requestedPath, options.source.entry) : null;
1100
1769
  const examples = filterDocsCodeExamples((requestedPath ? matchedPage ? [matchedPage] : [] : pages).flatMap((page) => extractDocsMcpCodeExamples(page)), {
1101
1770
  query,
@@ -1142,10 +1811,7 @@ async function createDocsMcpServer(options) {
1142
1811
  outputPreview: { resultCount: examples.length },
1143
1812
  metadata: { tool: "get_code_examples" }
1144
1813
  });
1145
- return { content: [{
1146
- type: "text",
1147
- text: JSON.stringify({ examples }, null, 2)
1148
- }] };
1814
+ return createStructuredTextResult({ examples });
1149
1815
  } catch (error) {
1150
1816
  const elapsed = durationMs(startedAt);
1151
1817
  await emitDocsAgentTraceEvent(options.observability, {
@@ -1165,12 +1831,115 @@ async function createDocsMcpServer(options) {
1165
1831
  throw error;
1166
1832
  }
1167
1833
  });
1834
+ if (resolved.tools.getContext) server.registerTool("get_context", {
1835
+ title: "Get budgeted docs context",
1836
+ 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.",
1837
+ inputSchema: getContextInputSchema,
1838
+ outputSchema: contextOutputSchema,
1839
+ annotations: { readOnlyHint: true }
1840
+ }, async ({ query, framework, version, locale, tokenBudget }) => {
1841
+ const startedAt = nowMs();
1842
+ const trace = createDocsAgentTraceContext("mcp.tool.get_context");
1843
+ const callSpanId = createDocsAgentTraceId("span");
1844
+ await emitDocsAgentTraceEvent(options.observability, {
1845
+ type: "tool.call",
1846
+ source: "mcp",
1847
+ traceId: trace.traceId,
1848
+ spanId: callSpanId,
1849
+ name: "get_context",
1850
+ startedAt: trace.startedAt,
1851
+ status: "started",
1852
+ locale,
1853
+ inputPreview: {
1854
+ queryLength: query.length,
1855
+ framework,
1856
+ version,
1857
+ locale,
1858
+ tokenBudget
1859
+ },
1860
+ metadata: { tool: "get_context" }
1861
+ });
1862
+ try {
1863
+ const result = await buildDocsMcpContext({
1864
+ pages: dedupePages(await getSourcePages(locale)),
1865
+ query,
1866
+ framework,
1867
+ version,
1868
+ locale,
1869
+ tokenBudget,
1870
+ entry: options.source.entry,
1871
+ siteTitle: options.source.siteTitle
1872
+ });
1873
+ const elapsed = durationMs(startedAt);
1874
+ await emitDocsAnalyticsEvent(options.analytics, {
1875
+ type: "mcp_tool",
1876
+ source: "mcp",
1877
+ locale,
1878
+ input: { query },
1879
+ properties: {
1880
+ tool: "get_context",
1881
+ queryLength: query.length,
1882
+ framework,
1883
+ version,
1884
+ tokenBudget,
1885
+ usedUtf8Bytes: result.budget.usedUtf8Bytes,
1886
+ conservativeTokenUpperBound: result.budget.conservativeTokenUpperBound,
1887
+ truncated: result.budget.truncated,
1888
+ resultCount: result.resultCount,
1889
+ durationMs: elapsed
1890
+ }
1891
+ });
1892
+ trackMcpTool("get_context", {
1893
+ locale,
1894
+ resultCount: result.resultCount
1895
+ });
1896
+ await emitDocsAgentTraceEvent(options.observability, {
1897
+ type: "tool.result",
1898
+ source: "mcp",
1899
+ traceId: trace.traceId,
1900
+ parentSpanId: callSpanId,
1901
+ name: "get_context",
1902
+ startedAt: trace.startedAt,
1903
+ endedAt: (/* @__PURE__ */ new Date()).toISOString(),
1904
+ durationMs: elapsed,
1905
+ status: "success",
1906
+ locale,
1907
+ outputPreview: {
1908
+ resultCount: result.resultCount,
1909
+ candidateCount: result.candidateCount,
1910
+ utf8Bytes: result.budget.usedUtf8Bytes,
1911
+ conservativeTokenUpperBound: result.budget.conservativeTokenUpperBound,
1912
+ truncated: result.budget.truncated
1913
+ },
1914
+ metadata: { tool: "get_context" }
1915
+ });
1916
+ return createStructuredTextResult(result, result.context || JSON.stringify(result, null, 2));
1917
+ } catch (error) {
1918
+ const elapsed = durationMs(startedAt);
1919
+ await emitDocsAgentTraceEvent(options.observability, {
1920
+ type: "tool.error",
1921
+ source: "mcp",
1922
+ traceId: trace.traceId,
1923
+ parentSpanId: callSpanId,
1924
+ name: "get_context",
1925
+ startedAt: trace.startedAt,
1926
+ endedAt: (/* @__PURE__ */ new Date()).toISOString(),
1927
+ durationMs: elapsed,
1928
+ status: "error",
1929
+ locale,
1930
+ outputPreview: { message: error instanceof Error ? error.message : "Unknown error" },
1931
+ metadata: { tool: "get_context" }
1932
+ });
1933
+ throw error;
1934
+ }
1935
+ });
1168
1936
  if (resolved.tools.readPage) server.registerTool("read_page", {
1169
1937
  title: "Read a docs page",
1170
- description: "Read a documentation page by slug or URL path.",
1938
+ description: "Read a documentation page by slug or URL path, optionally selecting one heading and limiting returned characters.",
1171
1939
  inputSchema: readPageInputSchema,
1940
+ outputSchema: readPageOutputSchema,
1172
1941
  annotations: { readOnlyHint: true }
1173
- }, async ({ path: requestedPath, locale }) => {
1942
+ }, async ({ path: requestedPath, locale, section, maxChars }) => {
1174
1943
  const startedAt = nowMs();
1175
1944
  const trace = createDocsAgentTraceContext("mcp.tool.read_page");
1176
1945
  const callSpanId = createDocsAgentTraceId("span");
@@ -1185,12 +1954,14 @@ async function createDocsMcpServer(options) {
1185
1954
  locale,
1186
1955
  inputPreview: {
1187
1956
  path: requestedPath,
1188
- locale
1957
+ locale,
1958
+ section,
1959
+ maxChars
1189
1960
  },
1190
1961
  metadata: { tool: "read_page" }
1191
1962
  });
1192
1963
  try {
1193
- const page = findDocsPage(dedupePages(await options.source.getPages(locale)), requestedPath, options.source.entry);
1964
+ const page = findDocsPage(dedupePages(await getSourcePages(locale)), requestedPath, options.source.entry);
1194
1965
  if (!page) {
1195
1966
  const elapsed = durationMs(startedAt);
1196
1967
  await emitDocsAnalyticsEvent(options.analytics, {
@@ -1248,7 +2019,59 @@ async function createDocsMcpServer(options) {
1248
2019
  isError: true
1249
2020
  };
1250
2021
  }
1251
- const document = renderPageDocument(page);
2022
+ const fullDocument = renderPageDocument(page);
2023
+ const sectionDocument = getDocsMcpSourceMarkdown(page);
2024
+ const selectedSection = section ? findDocsMarkdownSection(sectionDocument, section) ?? (sectionDocument !== fullDocument ? findDocsMarkdownSection(fullDocument, section) : void 0) : void 0;
2025
+ if (section && !selectedSection) {
2026
+ const sourceSections = parseDocsMarkdownSections(sectionDocument);
2027
+ const availableSections = (sourceSections.length > 0 ? sourceSections : parseDocsMarkdownSections(fullDocument)).map((item) => ({
2028
+ title: item.title,
2029
+ anchor: item.anchor
2030
+ }));
2031
+ const errorText = renderBoundedDocsMcpSectionError({
2032
+ requestedSection: section,
2033
+ pageUrl: page.url,
2034
+ availableSections,
2035
+ maxChars
2036
+ });
2037
+ const elapsed = durationMs(startedAt);
2038
+ trackMcpTool("read_page", {
2039
+ locale,
2040
+ resultCount: 0
2041
+ });
2042
+ await emitDocsAgentTraceEvent(options.observability, {
2043
+ type: "tool.error",
2044
+ source: "mcp",
2045
+ traceId: trace.traceId,
2046
+ parentSpanId: callSpanId,
2047
+ name: "read_page",
2048
+ startedAt: trace.startedAt,
2049
+ endedAt: (/* @__PURE__ */ new Date()).toISOString(),
2050
+ durationMs: elapsed,
2051
+ status: "error",
2052
+ locale,
2053
+ path: page.url,
2054
+ outputPreview: {
2055
+ found: true,
2056
+ sectionFound: false,
2057
+ section
2058
+ },
2059
+ metadata: {
2060
+ tool: "read_page",
2061
+ reason: "section_not_found"
2062
+ }
2063
+ });
2064
+ return {
2065
+ content: [{
2066
+ type: "text",
2067
+ text: errorText
2068
+ }],
2069
+ isError: true
2070
+ };
2071
+ }
2072
+ const selectedDocument = selectedSection?.content ?? fullDocument;
2073
+ const limitedDocument = limitDocsMcpText(selectedDocument, maxChars);
2074
+ const document = limitedDocument.text;
1252
2075
  const elapsed = durationMs(startedAt);
1253
2076
  await emitDocsAnalyticsEvent(options.analytics, {
1254
2077
  type: "agent_read",
@@ -1261,7 +2084,10 @@ async function createDocsMcpServer(options) {
1261
2084
  requestedPath,
1262
2085
  slug: page.slug,
1263
2086
  found: true,
2087
+ section: selectedSection?.title,
1264
2088
  contentLength: document.length,
2089
+ totalContentLength: selectedDocument.length,
2090
+ truncated: limitedDocument.truncated,
1265
2091
  durationMs: elapsed
1266
2092
  }
1267
2093
  });
@@ -1275,7 +2101,10 @@ async function createDocsMcpServer(options) {
1275
2101
  requestedPath,
1276
2102
  slug: page.slug,
1277
2103
  found: true,
2104
+ section: selectedSection?.title,
1278
2105
  contentLength: document.length,
2106
+ totalContentLength: selectedDocument.length,
2107
+ truncated: limitedDocument.truncated,
1279
2108
  durationMs: elapsed
1280
2109
  }
1281
2110
  });
@@ -1298,14 +2127,22 @@ async function createDocsMcpServer(options) {
1298
2127
  outputPreview: {
1299
2128
  found: true,
1300
2129
  chars: document.length,
2130
+ totalChars: selectedDocument.length,
2131
+ truncated: limitedDocument.truncated,
2132
+ section: selectedSection?.title,
1301
2133
  slug: page.slug
1302
2134
  },
1303
2135
  metadata: { tool: "read_page" }
1304
2136
  });
1305
- return { content: [{
1306
- type: "text",
1307
- text: document
1308
- }] };
2137
+ return createStructuredTextResult({
2138
+ page: toStructuredDocsMcpPage(page),
2139
+ document,
2140
+ section: selectedSection?.title,
2141
+ anchor: selectedSection?.anchor,
2142
+ chars: document.length,
2143
+ totalChars: selectedDocument.length,
2144
+ truncated: limitedDocument.truncated
2145
+ }, document);
1309
2146
  } catch (error) {
1310
2147
  const elapsed = durationMs(startedAt);
1311
2148
  await emitDocsAgentTraceEvent(options.observability, {
@@ -1348,10 +2185,14 @@ function createDocsMcpHttpHandler(options) {
1348
2185
  id: readJsonRpcId(await parseJsonBody(request)),
1349
2186
  data: { reason: "mcp_disabled" }
1350
2187
  }),
1351
- DELETE: async () => createJsonErrorResponse(404, disabledMessage)
2188
+ DELETE: async () => createJsonErrorResponse(404, disabledMessage),
2189
+ OPTIONS: async () => createJsonErrorResponse(404, disabledMessage)
1352
2190
  };
1353
- async function createStatelessTransport() {
1354
- const server = await createDocsMcpServer(options);
2191
+ async function createStatelessTransport(requestContext) {
2192
+ const server = await createDocsMcpServer({
2193
+ ...options,
2194
+ requestContext
2195
+ });
1355
2196
  const transport = new WebStandardStreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
1356
2197
  await server.connect(transport);
1357
2198
  return {
@@ -1362,15 +2203,38 @@ function createDocsMcpHttpHandler(options) {
1362
2203
  async function handle(request) {
1363
2204
  const url = new URL(request.url);
1364
2205
  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();
2206
+ const security = resolved.security ?? resolveDocsMcpSecurityConfig();
2207
+ const prepared = await prepareDocsMcpHttpRequest(request, security.maxBodyBytes);
2208
+ if (prepared.status === "too-large") return createMcpRequestTooLargeResponse(security.maxBodyBytes);
2209
+ request = prepared.request;
2210
+ let originAllowed;
2211
+ try {
2212
+ originAllowed = await isDocsMcpOriginAllowed(request.clone(), security.allowedOrigins);
1370
2213
  } catch {
1371
- bodyParseFailed = true;
1372
- parsedBody = void 0;
2214
+ return createMcpHttpSecurityErrorResponse(500, "MCP Origin policy failed");
1373
2215
  }
2216
+ if (!originAllowed) return createMcpHttpSecurityErrorResponse(403, "Forbidden Origin");
2217
+ if (method === "OPTIONS") return createDocsMcpOptionsResponse(request, security.cors);
2218
+ const withCors = (response) => applyDocsMcpCorsHeaders(response, request, security.cors);
2219
+ let auth;
2220
+ if (security.authenticate) {
2221
+ let authentication;
2222
+ try {
2223
+ authentication = await security.authenticate({
2224
+ request: request.clone(),
2225
+ pathname: url.pathname
2226
+ });
2227
+ } catch {
2228
+ return withCors(createMcpHttpSecurityErrorResponse(500, "MCP authentication failed"));
2229
+ }
2230
+ if (authentication instanceof Response) return withCors(authentication);
2231
+ if (authentication === null || authentication === void 0) return withCors(createMcpHttpSecurityErrorResponse(401, "Unauthorized"));
2232
+ if (!isDocsMcpAuthPrincipal(authentication)) return withCors(createMcpHttpSecurityErrorResponse(500, "MCP authentication returned an invalid principal"));
2233
+ auth = authentication;
2234
+ }
2235
+ const sessionId = request.headers.get("mcp-session-id") ?? request.headers.get("Mcp-Session-Id");
2236
+ const parsedBody = prepared.parsedBody;
2237
+ const bodyParseFailed = prepared.bodyParseFailed;
1374
2238
  const initializeRequest = method === "POST" && parsedBody && isInitializeRequest(parsedBody);
1375
2239
  emitDocsTelemetryProjectEvent(telemetryConfig, {
1376
2240
  framework: telemetryFramework,
@@ -1397,24 +2261,220 @@ function createDocsMcpHttpHandler(options) {
1397
2261
  initialize: Boolean(initializeRequest)
1398
2262
  }
1399
2263
  });
1400
- if (method === "POST" && bodyParseFailed) return createJsonRpcErrorResponse({
2264
+ if (method === "POST" && bodyParseFailed) return withCors(createJsonRpcErrorResponse({
1401
2265
  status: 400,
1402
2266
  code: -32700,
1403
2267
  message: "Parse error: Invalid JSON"
1404
- });
1405
- return (await createStatelessTransport()).transport.handleRequest(request, parsedBody === void 0 ? void 0 : { parsedBody });
2268
+ }));
2269
+ return withCors(await (await createStatelessTransport({
2270
+ transport: "http",
2271
+ request: request.clone(),
2272
+ auth
2273
+ })).transport.handleRequest(request, parsedBody === void 0 ? void 0 : { parsedBody }));
1406
2274
  }
1407
2275
  return {
1408
2276
  GET: async ({ request }) => handle(request),
1409
2277
  POST: async ({ request }) => handle(request),
1410
- DELETE: async ({ request }) => handle(request)
2278
+ DELETE: async ({ request }) => handle(request),
2279
+ OPTIONS: async ({ request }) => handle(request)
1411
2280
  };
1412
2281
  }
1413
2282
  async function runDocsMcpStdio(options) {
1414
- const server = await createDocsMcpServer(options);
2283
+ const server = await createDocsMcpServer({
2284
+ ...options,
2285
+ requestContext: { transport: "stdio" }
2286
+ });
1415
2287
  const transport = new StdioServerTransport();
1416
2288
  await server.connect(transport);
1417
2289
  }
2290
+ async function isDocsMcpOriginAllowed(request, allowedOrigins) {
2291
+ const origin = request.headers.get("origin");
2292
+ if (!origin) return true;
2293
+ if (typeof allowedOrigins === "function") return allowedOrigins({
2294
+ origin,
2295
+ request
2296
+ });
2297
+ const normalizedOrigin = normalizeHttpOrigin(origin);
2298
+ if (allowedOrigins === "same-origin") return normalizedOrigin === new URL(request.url).origin;
2299
+ return allowedOrigins.some((allowedOrigin) => normalizeHttpOrigin(allowedOrigin) === normalizedOrigin);
2300
+ }
2301
+ function normalizeHttpOrigin(value) {
2302
+ try {
2303
+ return new URL(value).origin;
2304
+ } catch {
2305
+ return value.trim();
2306
+ }
2307
+ }
2308
+ function isDocsMcpAuthPrincipal(value) {
2309
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
2310
+ const principal = value;
2311
+ if (typeof principal.id !== "string" || principal.id.trim().length === 0) return false;
2312
+ if (principal.scopes !== void 0 && (!Array.isArray(principal.scopes) || principal.scopes.some((scope) => typeof scope !== "string"))) return false;
2313
+ return principal.claims === void 0 || typeof principal.claims === "object" && principal.claims !== null && !Array.isArray(principal.claims);
2314
+ }
2315
+ async function prepareDocsMcpHttpRequest(request, maxBodyBytes) {
2316
+ if (request.method.toUpperCase() !== "POST") return {
2317
+ status: "ok",
2318
+ request,
2319
+ bodyParseFailed: false
2320
+ };
2321
+ if (isContentLengthOverLimit(request, maxBodyBytes)) return { status: "too-large" };
2322
+ const body = request.body;
2323
+ if (!body) return {
2324
+ status: "ok",
2325
+ request,
2326
+ bodyParseFailed: true
2327
+ };
2328
+ const reader = body.getReader();
2329
+ const chunks = [];
2330
+ let byteLength = 0;
2331
+ while (true) {
2332
+ const { done, value } = await reader.read();
2333
+ if (done) break;
2334
+ byteLength += value.byteLength;
2335
+ if (byteLength > maxBodyBytes) {
2336
+ reader.cancel();
2337
+ return { status: "too-large" };
2338
+ }
2339
+ chunks.push(value);
2340
+ }
2341
+ const bodyBytes = new Uint8Array(byteLength);
2342
+ let offset = 0;
2343
+ for (const chunk of chunks) {
2344
+ bodyBytes.set(chunk, offset);
2345
+ offset += chunk.byteLength;
2346
+ }
2347
+ const boundedRequest = new Request(request.url, {
2348
+ method: request.method,
2349
+ headers: new Headers(request.headers),
2350
+ body: bodyBytes,
2351
+ redirect: request.redirect,
2352
+ signal: request.signal
2353
+ });
2354
+ try {
2355
+ return {
2356
+ status: "ok",
2357
+ request: boundedRequest,
2358
+ parsedBody: JSON.parse(new TextDecoder().decode(bodyBytes)),
2359
+ bodyParseFailed: false
2360
+ };
2361
+ } catch {
2362
+ return {
2363
+ status: "ok",
2364
+ request: boundedRequest,
2365
+ bodyParseFailed: true
2366
+ };
2367
+ }
2368
+ }
2369
+ function isContentLengthOverLimit(request, maxBodyBytes) {
2370
+ const rawContentLength = request.headers.get("content-length");
2371
+ if (rawContentLength === null) return false;
2372
+ const contentLength = Number(rawContentLength);
2373
+ return Number.isFinite(contentLength) && contentLength > maxBodyBytes;
2374
+ }
2375
+ const DOCS_MCP_CORS_METHODS = [
2376
+ "GET",
2377
+ "POST",
2378
+ "DELETE",
2379
+ "OPTIONS"
2380
+ ];
2381
+ const DOCS_MCP_CORS_REQUEST_METHODS = new Set([
2382
+ "GET",
2383
+ "POST",
2384
+ "DELETE"
2385
+ ]);
2386
+ function createDocsMcpOptionsResponse(request, cors) {
2387
+ const allow = DOCS_MCP_CORS_METHODS.join(", ");
2388
+ if (!request.headers.get("origin") || !cors.enabled) return new Response(null, {
2389
+ status: 204,
2390
+ headers: { Allow: allow }
2391
+ });
2392
+ const requestedMethod = request.headers.get("access-control-request-method")?.trim().toUpperCase();
2393
+ if (requestedMethod && !DOCS_MCP_CORS_REQUEST_METHODS.has(requestedMethod)) return applyDocsMcpPreflightCorsHeaders(createMcpHttpSecurityErrorResponse(405, "CORS request method is not allowed"), request, cors);
2394
+ const allowedHeaders = new Set(cors.allowedHeaders.map((header) => header.toLowerCase()));
2395
+ const rejectedHeader = parseCorsRequestedHeaders(request.headers.get("access-control-request-headers")).find((header) => !allowedHeaders.has(header.toLowerCase()));
2396
+ if (rejectedHeader) return applyDocsMcpPreflightCorsHeaders(createMcpHttpSecurityErrorResponse(403, `CORS request header is not allowed: ${rejectedHeader}`), request, cors);
2397
+ return applyDocsMcpPreflightCorsHeaders(new Response(null, {
2398
+ status: 204,
2399
+ headers: { Allow: allow }
2400
+ }), request, cors);
2401
+ }
2402
+ function parseCorsRequestedHeaders(value) {
2403
+ if (!value) return [];
2404
+ return value.split(",").map((header) => header.trim()).filter(Boolean);
2405
+ }
2406
+ function applyDocsMcpPreflightCorsHeaders(response, request, cors) {
2407
+ const headers = buildDocsMcpCorsHeaders(response.headers, request, cors);
2408
+ headers.set("Access-Control-Allow-Methods", DOCS_MCP_CORS_METHODS.join(", "));
2409
+ headers.set("Access-Control-Allow-Headers", cors.allowedHeaders.join(", "));
2410
+ headers.set("Access-Control-Max-Age", String(cors.maxAgeSeconds));
2411
+ appendVaryHeader(headers, "Access-Control-Request-Method");
2412
+ appendVaryHeader(headers, "Access-Control-Request-Headers");
2413
+ return cloneResponseWithHeaders(response, headers);
2414
+ }
2415
+ function applyDocsMcpCorsHeaders(response, request, cors) {
2416
+ if (!request.headers.has("origin") || !cors.enabled) return response;
2417
+ const headers = buildDocsMcpCorsHeaders(response.headers, request, cors);
2418
+ if (!headers.has("Access-Control-Allow-Origin")) return response;
2419
+ if (cors.exposedHeaders.length > 0) headers.set("Access-Control-Expose-Headers", cors.exposedHeaders.join(", "));
2420
+ return cloneResponseWithHeaders(response, headers);
2421
+ }
2422
+ function buildDocsMcpCorsHeaders(source, request, cors) {
2423
+ const headers = new Headers(source);
2424
+ headers.delete("Access-Control-Allow-Origin");
2425
+ headers.delete("Access-Control-Allow-Credentials");
2426
+ const origin = serializeCorsOrigin(request.headers.get("origin"));
2427
+ if (!origin) return headers;
2428
+ headers.set("Access-Control-Allow-Origin", origin);
2429
+ if (cors.allowCredentials) headers.set("Access-Control-Allow-Credentials", "true");
2430
+ else headers.delete("Access-Control-Allow-Credentials");
2431
+ appendVaryHeader(headers, "Origin");
2432
+ return headers;
2433
+ }
2434
+ function serializeCorsOrigin(value) {
2435
+ if (!value) return null;
2436
+ if (value.trim() === "null") return "null";
2437
+ try {
2438
+ const origin = new URL(value).origin;
2439
+ return origin === "null" ? null : origin;
2440
+ } catch {
2441
+ return null;
2442
+ }
2443
+ }
2444
+ function appendVaryHeader(headers, value) {
2445
+ const values = (headers.get("Vary") ?? "").split(",").map((entry) => entry.trim()).filter(Boolean);
2446
+ if (!values.some((entry) => entry.toLowerCase() === value.toLowerCase())) values.push(value);
2447
+ headers.set("Vary", values.join(", "));
2448
+ }
2449
+ function cloneResponseWithHeaders(response, headers) {
2450
+ return new Response(response.body, {
2451
+ status: response.status,
2452
+ statusText: response.statusText,
2453
+ headers
2454
+ });
2455
+ }
2456
+ function createMcpRequestTooLargeResponse(maxBodyBytes) {
2457
+ const response = createJsonRpcErrorResponse({
2458
+ status: 413,
2459
+ code: -32e3,
2460
+ message: `Request body exceeds the ${maxBodyBytes} byte limit`,
2461
+ data: {
2462
+ reason: "request_too_large",
2463
+ maxBodyBytes
2464
+ }
2465
+ });
2466
+ response.headers.set("Cache-Control", "no-store");
2467
+ return response;
2468
+ }
2469
+ function createMcpHttpSecurityErrorResponse(status, error) {
2470
+ return new Response(JSON.stringify({ error }), {
2471
+ status,
2472
+ headers: {
2473
+ "Content-Type": "application/json",
2474
+ "Cache-Control": "no-store"
2475
+ }
2476
+ });
2477
+ }
1418
2478
  function createJsonErrorResponse(status, error) {
1419
2479
  return new Response(JSON.stringify({ error }), {
1420
2480
  status,
@@ -1570,9 +2630,14 @@ function scanFilesystemDocsPages(contentDirAbs, entry, rootDir) {
1570
2630
  title,
1571
2631
  description: data.description,
1572
2632
  relatedInput: data.related,
2633
+ agent: normalizePageAgentFrontmatter(data.agent),
1573
2634
  icon: data.icon,
1574
2635
  sourcePath: path.relative(rootDir, full).replace(/\\/g, "/"),
1575
2636
  lastModified: stat.mtime.toISOString(),
2637
+ locale: typeof data.locale === "string" ? data.locale : void 0,
2638
+ framework: typeof data.framework === "string" ? data.framework : void 0,
2639
+ version: typeof data.version === "string" ? data.version : void 0,
2640
+ tags: Array.isArray(data.tags) ? data.tags.filter((tag) => typeof tag === "string") : void 0,
1576
2641
  content: stripMarkdownForMcp(humanRawContent),
1577
2642
  rawContent: humanRawContent,
1578
2643
  agentFallbackContent: pageAgentContent,
@@ -1714,12 +2779,17 @@ function toSearchSourcePages(pages) {
1714
2779
  rawContent: page.agentRawContent ?? page.agentFallbackRawContent ?? page.rawContent,
1715
2780
  sourcePath: page.sourcePath,
1716
2781
  lastModified: page.lastModified,
2782
+ locale: page.locale,
2783
+ framework: page.framework,
2784
+ version: page.version,
2785
+ tags: page.tags,
1717
2786
  agentContent: page.agentContent,
1718
2787
  agentRawContent: page.agentRawContent,
1719
2788
  agentFallbackContent: page.agentFallbackContent,
1720
2789
  agentFallbackRawContent: page.agentFallbackRawContent,
1721
2790
  description: page.description,
1722
- related: page.related
2791
+ related: page.related,
2792
+ agent: page.agent
1723
2793
  }));
1724
2794
  }
1725
2795
  function getDocsConfigSchema(filters) {
@@ -1803,13 +2873,24 @@ function resolveMcpToolSearchConfig(search, route) {
1803
2873
  chunking: config.chunking
1804
2874
  };
1805
2875
  }
2876
+ function toAgentContractSummary(value) {
2877
+ const agent = normalizePageAgentFrontmatter(value);
2878
+ const hasContract = hasStructuredPageAgentContract(agent);
2879
+ return {
2880
+ hasContract,
2881
+ ...hasContract && agent?.task ? { task: agent.task } : {},
2882
+ ...hasContract && agent?.outcome ? { outcome: agent.outcome } : {},
2883
+ ...hasContract && agent?.appliesTo ? { appliesTo: agent.appliesTo } : {}
2884
+ };
2885
+ }
1806
2886
  function toPageSummaries(pages) {
1807
2887
  return pages.map((page) => ({
1808
2888
  slug: page.slug,
1809
2889
  url: page.url,
1810
2890
  title: page.title,
1811
2891
  description: page.description,
1812
- icon: page.icon
2892
+ icon: page.icon,
2893
+ agent: toAgentContractSummary(page.agent)
1813
2894
  }));
1814
2895
  }
1815
2896
  function toDocsListPageSummary(page) {
@@ -1818,11 +2899,47 @@ function toDocsListPageSummary(page) {
1818
2899
  url: page.url,
1819
2900
  title: page.title,
1820
2901
  description: page.description,
2902
+ agent: toAgentContractSummary(page.agent),
1821
2903
  icon: page.icon,
1822
2904
  sourcePath: page.sourcePath,
1823
2905
  lastModified: page.lastModified
1824
2906
  };
1825
2907
  }
2908
+ function listDocsTasks(pages, filters) {
2909
+ const query = filters.query?.toLowerCase();
2910
+ const applicabilityFilters = [
2911
+ ["framework", filters.framework],
2912
+ ["version", filters.version],
2913
+ ["package", filters.package]
2914
+ ];
2915
+ return pages.flatMap((page) => {
2916
+ const agent = normalizePageAgentFrontmatter(page.agent);
2917
+ if (!agent || !hasStructuredPageAgentContract(agent)) return [];
2918
+ for (const [field, expected] of applicabilityFilters) {
2919
+ if (!expected) continue;
2920
+ const actualValue = agent.appliesTo?.[field];
2921
+ if (!(typeof actualValue === "string" ? [actualValue] : actualValue ?? []).some((value) => value.toLowerCase() === expected.toLowerCase())) return [];
2922
+ }
2923
+ if (query) {
2924
+ if (![
2925
+ page.slug,
2926
+ page.url,
2927
+ page.title,
2928
+ page.description,
2929
+ JSON.stringify(agent)
2930
+ ].filter(Boolean).join("\n").toLowerCase().includes(query)) return [];
2931
+ }
2932
+ return [{
2933
+ slug: page.slug,
2934
+ url: page.url,
2935
+ title: page.title,
2936
+ ...page.description ? { description: page.description } : {},
2937
+ ...agent.task ? { task: agent.task } : {},
2938
+ ...agent.outcome ? { outcome: agent.outcome } : {},
2939
+ ...agent.appliesTo ? { appliesTo: agent.appliesTo } : {}
2940
+ }];
2941
+ });
2942
+ }
1826
2943
  function listDocsBySection(pages, filters) {
1827
2944
  const allPages = pages.map(toDocsListPageSummary);
1828
2945
  const tree = buildDocsSectionTree(pages);
@@ -2113,6 +3230,269 @@ function getCodeExampleSearchText(example) {
2113
3230
  example.code
2114
3231
  ].filter((value) => typeof value === "string").join("\n");
2115
3232
  }
3233
+ function limitDocsMcpText(value, maxChars) {
3234
+ if (maxChars === void 0 || value.length <= maxChars) return {
3235
+ text: value,
3236
+ truncated: false
3237
+ };
3238
+ if (maxChars <= 1) return {
3239
+ text: value.slice(0, maxChars),
3240
+ truncated: true
3241
+ };
3242
+ const suffix = "…";
3243
+ const available = Math.max(0, maxChars - 1);
3244
+ let selected = value.slice(0, available);
3245
+ const paragraphBreak = selected.lastIndexOf("\n\n");
3246
+ const lineBreak = selected.lastIndexOf("\n");
3247
+ const preferredBreak = paragraphBreak >= available * .6 ? paragraphBreak : lineBreak;
3248
+ if (preferredBreak >= available * .6) selected = selected.slice(0, preferredBreak);
3249
+ return {
3250
+ text: `${selected.trimEnd()}${suffix}`.slice(0, maxChars),
3251
+ truncated: true
3252
+ };
3253
+ }
3254
+ function docsMcpUtf8Bytes(value) {
3255
+ return UTF8_ENCODER.encode(value).byteLength;
3256
+ }
3257
+ function limitDocsMcpUtf8Bytes(value, maxUtf8Bytes) {
3258
+ if (docsMcpUtf8Bytes(value) <= maxUtf8Bytes) return {
3259
+ text: value,
3260
+ truncated: false
3261
+ };
3262
+ if (maxUtf8Bytes <= 0) return {
3263
+ text: "",
3264
+ truncated: true
3265
+ };
3266
+ const suffix = "…";
3267
+ const suffixBytes = docsMcpUtf8Bytes(suffix);
3268
+ const availableBytes = Math.max(0, maxUtf8Bytes - suffixBytes);
3269
+ let selected = "";
3270
+ let selectedBytes = 0;
3271
+ for (const character of value) {
3272
+ const characterBytes = docsMcpUtf8Bytes(character);
3273
+ if (selectedBytes + characterBytes > availableBytes) break;
3274
+ selected += character;
3275
+ selectedBytes += characterBytes;
3276
+ }
3277
+ const paragraphBreak = selected.lastIndexOf("\n\n");
3278
+ const lineBreak = selected.lastIndexOf("\n");
3279
+ const preferredBreak = paragraphBreak >= selected.length * .6 ? paragraphBreak : lineBreak;
3280
+ if (preferredBreak >= selected.length * .6) selected = selected.slice(0, preferredBreak);
3281
+ if (suffixBytes > maxUtf8Bytes) return {
3282
+ text: ".".repeat(maxUtf8Bytes),
3283
+ truncated: true
3284
+ };
3285
+ return {
3286
+ text: `${selected.trimEnd()}${suffix}`,
3287
+ truncated: true
3288
+ };
3289
+ }
3290
+ function renderBoundedDocsMcpSectionError(options) {
3291
+ const maxChars = options.maxChars ?? Number.POSITIVE_INFINITY;
3292
+ const shorten = (value, max) => limitDocsMcpText(value, max).text;
3293
+ const availableSections = [];
3294
+ const base = {
3295
+ error: "section_not_found",
3296
+ message: `No section matched "${shorten(options.requestedSection, 80)}" in "${shorten(options.pageUrl, 80)}".`,
3297
+ availableSections,
3298
+ truncated: false
3299
+ };
3300
+ const serialize = () => JSON.stringify(base, null, 2);
3301
+ for (const section of options.availableSections) {
3302
+ availableSections.push({
3303
+ title: shorten(section.title, 80),
3304
+ anchor: shorten(section.anchor, 80)
3305
+ });
3306
+ if (serialize().length > maxChars) {
3307
+ availableSections.pop();
3308
+ base.truncated = true;
3309
+ break;
3310
+ }
3311
+ }
3312
+ if (availableSections.length < options.availableSections.length) base.truncated = true;
3313
+ if (serialize().length <= maxChars) return serialize();
3314
+ const minimalText = JSON.stringify({
3315
+ error: "section_not_found",
3316
+ message: "The requested section was not found.",
3317
+ availableSections: [],
3318
+ truncated: true
3319
+ });
3320
+ if (minimalText.length <= maxChars) return minimalText;
3321
+ return JSON.stringify({ error: "section_not_found" });
3322
+ }
3323
+ function toStructuredDocsMcpPage(page) {
3324
+ return {
3325
+ slug: page.slug,
3326
+ url: page.url,
3327
+ title: page.title,
3328
+ description: page.description,
3329
+ related: page.related,
3330
+ icon: page.icon,
3331
+ sourcePath: page.sourcePath,
3332
+ lastModified: page.lastModified,
3333
+ locale: page.locale,
3334
+ framework: page.framework,
3335
+ version: page.version,
3336
+ tags: page.tags
3337
+ };
3338
+ }
3339
+ function getDocsMcpSourceMarkdown(page) {
3340
+ return page.agentRawContent ?? page.agentFallbackRawContent ?? page.rawContent ?? page.agentContent ?? page.agentFallbackContent ?? page.content;
3341
+ }
3342
+ function normalizeDocsMcpFramework(value) {
3343
+ const normalized = value.toLowerCase().replace(/[^a-z0-9]/gu, "");
3344
+ if (["next", "nextjs"].includes(normalized)) return "nextjs";
3345
+ if (["tanstack", "tanstackstart"].includes(normalized)) return "tanstackstart";
3346
+ if (["svelte", "sveltekit"].includes(normalized)) return "sveltekit";
3347
+ return normalized;
3348
+ }
3349
+ function normalizeDocsMcpVersion(value) {
3350
+ return value.trim().toLowerCase().replace(/^v(?=\d)/u, "");
3351
+ }
3352
+ function normalizeDocsMcpLocale(value) {
3353
+ return value.trim().toLowerCase().replace(/_/gu, "-");
3354
+ }
3355
+ function pageMatchesDocsMcpContextScope(page, filters) {
3356
+ if (filters.framework && page.framework && normalizeDocsMcpFramework(page.framework) !== normalizeDocsMcpFramework(filters.framework)) return false;
3357
+ if (filters.version && page.version && normalizeDocsMcpVersion(page.version) !== normalizeDocsMcpVersion(filters.version)) return false;
3358
+ if (filters.locale && page.locale && normalizeDocsMcpLocale(page.locale) !== normalizeDocsMcpLocale(filters.locale)) return false;
3359
+ return true;
3360
+ }
3361
+ function getDocsMcpResultPageUrl(value) {
3362
+ return value.split("#", 1)[0] ?? value;
3363
+ }
3364
+ function getDocsMcpResultAnchor(value) {
3365
+ const anchor = value.split("#", 2)[1];
3366
+ if (!anchor) return void 0;
3367
+ try {
3368
+ return decodeURIComponent(anchor);
3369
+ } catch {
3370
+ return anchor;
3371
+ }
3372
+ }
3373
+ async function buildDocsMcpContext(options) {
3374
+ const scopedPages = options.pages.filter((page) => pageMatchesDocsMcpContextScope(page, {
3375
+ framework: options.framework,
3376
+ version: options.version,
3377
+ locale: options.locale
3378
+ }));
3379
+ const orderedResults = [...await performDocsSearch({
3380
+ pages: toSearchSourcePages(scopedPages),
3381
+ query: options.query,
3382
+ search: {
3383
+ enabled: true,
3384
+ provider: "simple",
3385
+ maxResults: 50,
3386
+ chunking: { strategy: "section" }
3387
+ },
3388
+ locale: options.locale,
3389
+ siteTitle: options.siteTitle,
3390
+ limit: 50
3391
+ })].sort((left, right) => {
3392
+ const scoreDelta = (right.score ?? 0) - (left.score ?? 0);
3393
+ if (scoreDelta !== 0) return scoreDelta;
3394
+ if (left.url !== right.url) return left.url < right.url ? -1 : 1;
3395
+ if ((left.section ?? "") !== (right.section ?? "")) return (left.section ?? "") < (right.section ?? "") ? -1 : 1;
3396
+ if (left.id === right.id) return 0;
3397
+ return left.id < right.id ? -1 : 1;
3398
+ });
3399
+ const sectionPageUrls = new Set(orderedResults.filter((result) => result.section).map((result) => normalizeUrlPath(getDocsMcpResultPageUrl(result.url))));
3400
+ const seen = /* @__PURE__ */ new Set();
3401
+ const resolvedCandidates = orderedResults.filter((result) => {
3402
+ const pageUrl = normalizeUrlPath(getDocsMcpResultPageUrl(result.url));
3403
+ if (!result.section && sectionPageUrls.has(pageUrl)) return false;
3404
+ const key = `${pageUrl}#${getDocsMcpResultAnchor(result.url) ?? result.section ?? ""}`;
3405
+ if (seen.has(key)) return false;
3406
+ seen.add(key);
3407
+ return true;
3408
+ }).flatMap((result) => {
3409
+ const page = findDocsPage(scopedPages, getDocsMcpResultPageUrl(result.url), options.entry);
3410
+ if (!page) return [];
3411
+ const document = upsertPageAgentContractMarkdown(getDocsMcpSourceMarkdown(page), page.agent);
3412
+ const resultAnchor = getDocsMcpResultAnchor(result.url);
3413
+ const selectedSection = resultAnchor ? findDocsMarkdownSection(document, resultAnchor) : result.section ? findDocsMarkdownSection(document, result.section) : void 0;
3414
+ if ((resultAnchor || result.section) && !selectedSection) return [];
3415
+ const rawContent = (selectedSection?.content ?? document).trim();
3416
+ if (!rawContent) return [];
3417
+ return [{
3418
+ result,
3419
+ page,
3420
+ selectedSection,
3421
+ rawContent
3422
+ }];
3423
+ });
3424
+ const maxUtf8Bytes = options.tokenBudget;
3425
+ const separator = "\n\n---\n\n";
3426
+ const separatorUtf8Bytes = docsMcpUtf8Bytes(separator);
3427
+ const blocks = [];
3428
+ const sources = [];
3429
+ let usedUtf8Bytes = 0;
3430
+ for (const { result, page, selectedSection, rawContent } of resolvedCandidates) {
3431
+ const anchor = selectedSection?.anchor;
3432
+ const sourceUrl = anchor ? `${page.url}#${anchor}` : page.url;
3433
+ const headerLines = [`## ${page.title}`, `Source: ${sourceUrl}`];
3434
+ if (selectedSection?.title) headerLines.push(`Section: ${selectedSection.title}`);
3435
+ if (page.framework) headerLines.push(`Framework: ${page.framework}`);
3436
+ if (page.version) headerLines.push(`Version: ${page.version}`);
3437
+ if (page.locale) headerLines.push(`Locale: ${page.locale}`);
3438
+ const header = headerLines.join("\n");
3439
+ const separatorBytes = blocks.length === 0 ? 0 : separatorUtf8Bytes;
3440
+ const headerBytes = docsMcpUtf8Bytes(`${header}\n\n`);
3441
+ const availableForContent = maxUtf8Bytes - usedUtf8Bytes - separatorBytes - headerBytes;
3442
+ if (availableForContent <= 0) break;
3443
+ const limited = limitDocsMcpUtf8Bytes(rawContent, availableForContent);
3444
+ if (!limited.text) break;
3445
+ const block = `${header}\n\n${limited.text}`;
3446
+ const blockUtf8Bytes = docsMcpUtf8Bytes(block);
3447
+ blocks.push(block);
3448
+ usedUtf8Bytes += separatorBytes + blockUtf8Bytes;
3449
+ sources.push({
3450
+ id: result.id,
3451
+ title: page.title,
3452
+ pageUrl: page.url,
3453
+ url: sourceUrl,
3454
+ section: selectedSection?.title,
3455
+ anchor,
3456
+ sourcePath: page.sourcePath,
3457
+ lastModified: page.lastModified,
3458
+ locale: page.locale,
3459
+ framework: page.framework,
3460
+ version: page.version,
3461
+ tags: page.tags,
3462
+ score: result.score,
3463
+ content: limited.text,
3464
+ chars: limited.text.length,
3465
+ utf8Bytes: docsMcpUtf8Bytes(limited.text),
3466
+ truncated: limited.truncated
3467
+ });
3468
+ if (limited.truncated) break;
3469
+ }
3470
+ const context = blocks.join(separator);
3471
+ usedUtf8Bytes = docsMcpUtf8Bytes(context);
3472
+ const remainingUtf8Bytes = Math.max(0, maxUtf8Bytes - usedUtf8Bytes);
3473
+ const truncated = sources.some((source) => source.truncated) || sources.length < resolvedCandidates.length;
3474
+ return {
3475
+ query: options.query,
3476
+ filters: {
3477
+ framework: options.framework,
3478
+ version: options.version,
3479
+ locale: options.locale
3480
+ },
3481
+ budget: {
3482
+ requestedTokens: options.tokenBudget,
3483
+ strategy: "utf8-bytes",
3484
+ maxUtf8Bytes,
3485
+ usedUtf8Bytes,
3486
+ conservativeTokenUpperBound: usedUtf8Bytes,
3487
+ remainingUtf8Bytes,
3488
+ truncated
3489
+ },
3490
+ resultCount: sources.length,
3491
+ candidateCount: resolvedCandidates.length,
3492
+ context,
3493
+ sources
3494
+ };
3495
+ }
2116
3496
  function findDocsPage(pages, requestedPath, entry) {
2117
3497
  const normalizedRequest = normalizeRequestedPath(requestedPath, entry);
2118
3498
  for (const page of pages) if (normalizeUrlPath(page.url) === normalizedRequest) return page;
@@ -2141,12 +3521,12 @@ function normalizeUrlPath(value) {
2141
3521
  return normalized.replace(/\/+$/, "");
2142
3522
  }
2143
3523
  function renderPageDocument(page) {
2144
- if (page.agentRawContent !== void 0) return page.agentRawContent;
3524
+ if (page.agentRawContent !== void 0) return upsertPageAgentContractMarkdown(page.agentRawContent, page.agent);
2145
3525
  const relatedLines = renderDocsRelatedMarkdownLines(page.related);
2146
3526
  const lines = [`# ${page.title}`, `URL: ${page.url}`];
2147
3527
  if (page.description) lines.push(`Description: ${page.description}`);
2148
3528
  lines.push(...relatedLines);
2149
- lines.push("", page.agentFallbackRawContent ?? page.rawContent ?? page.content);
3529
+ lines.push("", upsertPageAgentContractMarkdown(page.agentFallbackRawContent ?? page.rawContent ?? page.content, page.agent));
2150
3530
  return lines.join("\n");
2151
3531
  }
2152
3532
  function renderNavigationTree(tree) {
@@ -2175,4 +3555,4 @@ function toPageResourceUri(url) {
2175
3555
  }
2176
3556
 
2177
3557
  //#endregion
2178
- export { createDocsMcpHttpHandler, createDocsMcpServer, createFilesystemDocsMcpSource, normalizeDocsMcpRoute, resolveDocsMcpConfig, runDocsMcpStdio };
3558
+ 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, createDocsMcpHttpHandler, createDocsMcpServer, createFilesystemDocsMcpSource, normalizeDocsMcpRoute, resolveDocsMcpConfig, runDocsMcpStdio };