@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.
- package/dist/agent-2t4kZLGo.mjs +10 -0
- package/dist/{agent-CaOBIVFy.mjs → agent-BMxMpEa0.mjs} +194 -22
- package/dist/agent-contract-b0akaPcg.mjs +573 -0
- package/dist/agent-export--X9Go-wj.mjs +853 -0
- package/dist/{agents-DjhjZaNP.mjs → agents-CW-zTNKP.mjs} +6 -5
- package/dist/cli/index.mjs +43 -28
- package/dist/client/react.d.mts +1 -1
- package/dist/{cloud-ask-ai-DcMe6xOf.d.mts → cloud-ask-ai-CMzYVzVX.d.mts} +1 -1
- package/dist/{cloud-C_Ok9rC5.mjs → cloud-zs0qAnKJ.mjs} +3 -3
- package/dist/{codeblocks-z9iT794h.mjs → codeblocks-DWM4IJPL.mjs} +9 -12
- package/dist/{dev-Tm9Ss4D6.mjs → dev-DFeVU2Ui.mjs} +2 -2
- package/dist/docs-cloud-server.d.mts +2 -2
- package/dist/docs-cloud-server.mjs +1 -1
- package/dist/{doctor-O9o9kjN8.mjs → doctor-CkM5tKdD.mjs} +36 -24
- package/dist/{downgrade-CYEaOctn.mjs → downgrade-Dj_KXV11.mjs} +2 -2
- package/dist/i18n-CAlj1ADU.mjs +40 -0
- package/dist/index.d.mts +139 -3
- package/dist/index.mjs +7 -5
- package/dist/{init-Ch5On0zm.mjs → init-Cmz-oqnp.mjs} +3 -3
- package/dist/{mcp-DuCPNHS-.mjs → mcp-B4Q1PN93.mjs} +9 -5
- package/dist/mcp.d.mts +102 -4
- package/dist/mcp.mjs +1446 -66
- package/dist/{package-version-C8PigBZk.mjs → package-version-DxnTZH4m.mjs} +1 -1
- package/dist/{reading-time-CPAy1SWO.mjs → reading-time-BuK2YL23.mjs} +278 -33
- package/dist/{review-lm3dt7yy.mjs → review-BEsbm0l2.mjs} +38 -3
- package/dist/{robots-DV7u4Ire.mjs → robots-BOwkq9du.mjs} +5 -4
- package/dist/{robots-XVPs9lVz.mjs → robots-C6uQG-fS.mjs} +1 -1
- package/dist/{search-BWuU70B0.mjs → search-CJNLkdjX.mjs} +174 -79
- package/dist/{search-DFEhf9-s.d.mts → search-CTu6Bvq-.d.mts} +3 -1
- package/dist/{search-B6nEkx86.mjs → search-CtAXkk_J.mjs} +5 -4
- package/dist/server.d.mts +5 -5
- package/dist/server.mjs +8 -7
- package/dist/{sitemap-BEiKy4Iy.mjs → sitemap-OPb8EdNe.mjs} +6 -5
- package/dist/{sitemap-server-xEHtDUDB.mjs → sitemap-server-DnilMEiZ.mjs} +1 -1
- package/dist/{templates-uaauJcTO.mjs → templates-Bty9M6lA.mjs} +11 -5
- package/dist/{types-XqGLsmOD.d.mts → types-CdKu8KmO.d.mts} +118 -2
- package/dist/{upgrade-DzQtpgJM.mjs → upgrade-BNdBB9tQ.mjs} +2 -2
- package/package.json +1 -1
- package/dist/agent-DXDOMJBw.mjs +0 -9
- /package/dist/{cloud-ask-ai-Cl-xaV8j.mjs → cloud-ask-ai-BFlx8VMO.mjs} +0 -0
- /package/dist/{config-tiQCZ46q.mjs → config-DhIyFsy9.mjs} +0 -0
- /package/dist/{errors-CW1LnxaQ.mjs → errors-BxfYhp1L.mjs} +0 -0
- /package/dist/{review-B7goPYUb.mjs → review-BHdy75TE.mjs} +0 -0
- /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 {
|
|
3
|
-
import {
|
|
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
|
|
646
|
-
const defaultTree = await
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
878
|
-
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
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 {
|
|
1306
|
-
|
|
1307
|
-
|
|
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(
|
|
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
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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 };
|