@farming-labs/docs 0.2.59 → 0.2.61

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 (56) hide show
  1. package/README.md +1 -1
  2. package/dist/{agent-DlxriaTs.mjs → agent-DFHlw_JC.mjs} +3 -3
  3. package/dist/{agent-BFqyqEnC.mjs → agent-Fl0pjVNF.mjs} +136 -331
  4. package/dist/{agent-evals-kJs2Y9xR.mjs → agent-evals-f4_OL10T.mjs} +3 -3
  5. package/dist/{agent-export-BgUaiW8f.mjs → agent-export-D0zQhasD.mjs} +87 -46
  6. package/dist/agent-skills-bundle.d.mts +13 -0
  7. package/dist/agent-skills-bundle.mjs +12 -0
  8. package/dist/agent-skills-server-CIeBszxp.mjs +263 -0
  9. package/dist/agent-skills-server-CKq3_jMj.d.mts +12 -0
  10. package/dist/agent-skills-vite.d.mts +31 -0
  11. package/dist/agent-skills-vite.mjs +70 -0
  12. package/dist/{agents-Djh-HXih.mjs → agents-ibnXrWyp.mjs} +9 -7
  13. package/dist/cli/index.mjs +32 -30
  14. package/dist/client/react.d.mts +1 -1
  15. package/dist/{cloud-ask-ai-B2WnG4fF.d.mts → cloud-ask-ai-D7ZgF47y.d.mts} +1 -1
  16. package/dist/{cloud-BH_sHX64.mjs → cloud-pdNC-tyj.mjs} +3 -3
  17. package/dist/{codeblocks-Bq67u32v.mjs → codeblocks-CFuurVIH.mjs} +2 -2
  18. package/dist/{config-DASewQ0x.mjs → config-Wcdj-D0a.mjs} +7 -1
  19. package/dist/{dev-CAQlbguk.mjs → dev-Cmy6DtdF.mjs} +2 -2
  20. package/dist/docs-cloud-server.d.mts +2 -2
  21. package/dist/docs-cloud-server.mjs +1 -1
  22. package/dist/{doctor-CO1VMcF_.mjs → doctor-CXostbsI.mjs} +151 -21
  23. package/dist/{downgrade-BZs86NVr.mjs → downgrade-w7e6Se0L.mjs} +2 -2
  24. package/dist/{golden-evaluations-BN9u2wxw.mjs → golden-evaluations-CBZ_JZjf.mjs} +19 -4
  25. package/dist/index.d.mts +52 -10
  26. package/dist/index.mjs +8 -7
  27. package/dist/{init-B_9ENq8Z.mjs → init-CQY0Woe3.mjs} +36 -5
  28. package/dist/mcp-DyPcoLwm.mjs +156 -0
  29. package/dist/mcp.d.mts +13 -2
  30. package/dist/mcp.mjs +699 -408
  31. package/dist/{metadata-BDuewuzq.mjs → metadata-Dv1ah0Aj.mjs} +1 -1
  32. package/dist/{package-version-qik_4J6C.mjs → package-version-n5AFur8a.mjs} +1 -1
  33. package/dist/{reading-time-BkEft6SD.mjs → reading-time-C-SAhQT9.mjs} +10 -9
  34. package/dist/{review-NC-sOdXn.mjs → review-D2UBrxFq.mjs} +18 -10
  35. package/dist/{robots-DskPvGPw.mjs → robots-5Yqz9mz7.mjs} +5 -4
  36. package/dist/{robots-ltltiLJF.mjs → robots-C08kDLsz.mjs} +6 -2
  37. package/dist/{search-D57JXQLj.mjs → search-9OnMGMvt.mjs} +1 -1
  38. package/dist/{search-C1JitPwi.d.mts → search-C6heDO8h.d.mts} +3 -126
  39. package/dist/{search-o4Ud6OXv.mjs → search-CNsRpz90.mjs} +8 -6
  40. package/dist/server.d.mts +6 -5
  41. package/dist/server.mjs +9 -7
  42. package/dist/{sitemap-Cq-Yj_iA.mjs → sitemap-BpYnSsfJ.mjs} +9 -7
  43. package/dist/{sitemap-server-C1ibVKOy.mjs → sitemap-server-D_0Kzanj.mjs} +1 -1
  44. package/dist/standards-discovery-C4HUqMd2.d.mts +227 -0
  45. package/dist/standards-discovery-C54V_aJH.mjs +510 -0
  46. package/dist/{templates-CfQjpDWW.mjs → templates-Bq_P7ctv.mjs} +102 -9
  47. package/dist/{types-XHABMh_f.d.mts → types-EhFhYGfr.d.mts} +58 -1
  48. package/dist/{upgrade-BCJTCW3O.mjs → upgrade-oz-GChgt.mjs} +2 -2
  49. package/package.json +11 -1
  50. package/dist/mcp-BAJr3wC2.mjs +0 -104
  51. /package/dist/{agent-scope-CCaIY1aK.mjs → agent-scope-C_U--OZ7.mjs} +0 -0
  52. /package/dist/{cloud-ask-ai-hnJfj8-X.mjs → cloud-ask-ai-sbpjOR2K.mjs} +0 -0
  53. /package/dist/{code-blocks-qe0T8-xe.mjs → code-blocks-DnNVNK2M.mjs} +0 -0
  54. /package/dist/{errors-CVqZ3kOO.mjs → errors-DbOhkE1h.mjs} +0 -0
  55. /package/dist/{i18n-CAlj1ADU.mjs → i18n-CCaFUnAN.mjs} +0 -0
  56. /package/dist/{utils-6UCLxv4B.mjs → utils-DpiIioYb.mjs} +0 -0
package/dist/mcp.mjs CHANGED
@@ -1,7 +1,8 @@
1
1
  import { a as emitDocsAnalyticsEvent, i as emitDocsAgentTraceEvent, n as createDocsAgentTraceContext, r as createDocsAgentTraceId } from "./analytics-Bx44lg6d.mjs";
2
- import { Ot as resolveDocsAudienceMdxContent, Sn as upsertPageAgentContractMarkdown, _n as hasStructuredPageAgentContract, ln as normalizeDocsRelated, un as renderDocsRelatedMarkdownLines, vn as normalizePageAgentFrontmatter } from "./agent-BFqyqEnC.mjs";
3
- import { C as resolvePageSidebarFolderIndexBehavior, E as emitDocsTelemetryAgentSurfaceEvent, O as emitDocsTelemetryMcpToolEvent, k as emitDocsTelemetryProjectEvent, m as parseDocsMarkdownSections, p as findDocsMarkdownSection, u as performDocsSearch, x as stripGeneratedAgentProvenance } from "./search-D57JXQLj.mjs";
4
- import { a as normalizeAgentScopeValues, i as normalizeAgentLocale, n as agentVersionConstraintsOverlap, r as normalizeAgentFramework, t as agentVersionConstraintMatches } from "./agent-scope-CCaIY1aK.mjs";
2
+ import { Bt as renderDocsRelatedMarkdownLines, Jt as normalizePageAgentFrontmatter, Qt as upsertPageAgentContractMarkdown, cn as isDocsMcpProtectedResourceMetadataPath, dn as normalizeDocsMcpAuthorizationServerUrls, fn as normalizeDocsMcpEndpointPath, mn as resolveDocsMcpResourceLocation, pn as resolveDocsMcpProtectedResourceMetadataLocation, qt as hasStructuredPageAgentContract, sn as isDocsMcpOAuthScopeToken, un as isDocsMcpResourcePath, wt as resolveDocsAudienceMdxContent, zt as normalizeDocsRelated } from "./agent-Fl0pjVNF.mjs";
3
+ import "./standards-discovery-C54V_aJH.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-9OnMGMvt.mjs";
5
+ import { a as normalizeAgentScopeValues, i as normalizeAgentLocale, n as agentVersionConstraintsOverlap, r as normalizeAgentFramework, t as agentVersionConstraintMatches } from "./agent-scope-C_U--OZ7.mjs";
5
6
  import matter from "gray-matter";
6
7
  import fs from "node:fs";
7
8
  import path from "node:path";
@@ -12,7 +13,6 @@ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
12
13
  import * as z from "zod/v4";
13
14
 
14
15
  //#region src/mcp.ts
15
- const DEFAULT_MCP_ROUTE = "/api/docs/mcp";
16
16
  const DEFAULT_MCP_VERSION = "0.0.0";
17
17
  const DEFAULT_MCP_NAME = "@farming-labs/docs";
18
18
  const DEFAULT_MCP_CONTEXT_TOKEN_BUDGET = 4e3;
@@ -222,405 +222,490 @@ const DOCS_CONFIG_SCHEMA_OPTIONS_TEMPLATE = [
222
222
  type: "DocsAgentConfig",
223
223
  description: "Agent compaction defaults and offline-by-default usefulness evaluations.",
224
224
  docs: "/docs/getting-started/agent-ready-docs",
225
- children: [{
226
- path: "agent.compact",
227
- name: "compact",
228
- type: "DocsAgentCompactConfig",
229
- description: "Defaults for generated agent.md compaction.",
230
- children: [
231
- {
232
- path: "agent.compact.apiKey",
233
- name: "apiKey",
234
- type: "string",
235
- description: "Direct compaction provider API key; prefer apiKeyEnv."
236
- },
237
- {
238
- path: "agent.compact.apiKeyEnv",
239
- name: "apiKeyEnv",
240
- type: "string",
241
- description: "Environment variable containing the compaction provider API key."
242
- },
243
- {
244
- path: "agent.compact.baseUrl",
245
- name: "baseUrl",
246
- type: "string",
247
- description: "Compaction provider base URL."
248
- },
249
- {
250
- path: "agent.compact.model",
251
- name: "model",
225
+ children: [
226
+ {
227
+ path: "agent.skills",
228
+ name: "skills",
229
+ type: "string | readonly string[] | DocsAgentSkillsConfig",
230
+ description: "Project skill files or collection directories published through discovery, static export, and MCP.",
231
+ children: [{
232
+ path: "agent.skills.paths",
233
+ name: "paths",
234
+ type: "string | readonly string[]",
235
+ description: "Workspace-contained SKILL.md file, skill directory, or collection directory paths."
236
+ }, {
237
+ path: "agent.skills.paths[]",
238
+ name: "path",
252
239
  type: "string",
253
- description: "Compaction model identifier."
254
- },
255
- {
256
- path: "agent.compact.aggressiveness",
257
- name: "aggressiveness",
258
- type: "number",
259
- default: .3,
260
- description: "Compression aggressiveness from 0 to 1."
261
- },
262
- {
263
- path: "agent.compact.maxOutputTokens",
264
- name: "maxOutputTokens",
265
- type: "number",
266
- description: "Upper output token target."
267
- },
268
- {
269
- path: "agent.compact.minOutputTokens",
270
- name: "minOutputTokens",
271
- type: "number",
272
- description: "Lower output token target."
273
- },
274
- {
275
- path: "agent.compact.protectJson",
276
- name: "protectJson",
277
- type: "boolean",
278
- description: "Preserve JSON objects during compaction when supported."
279
- }
280
- ]
281
- }, {
282
- path: "agent.evaluations",
283
- name: "evaluations",
284
- type: "boolean | DocsAgentEvaluationsConfig",
285
- description: "Offline-by-default golden tasks for retrieval, citation, version, example, answer, and budget evaluation, with explicit external-provider and execution opt-ins.",
286
- children: [
287
- {
288
- path: "agent.evaluations.enabled",
289
- name: "enabled",
290
- type: "boolean",
291
- default: true,
292
- description: "Enable configured golden-task evaluation."
293
- },
294
- {
295
- path: "agent.evaluations.tokenBudget",
296
- name: "tokenBudget",
297
- type: "number",
298
- default: 4e3,
299
- description: "Default hard UTF-8 context-byte ceiling for golden tasks."
300
- },
301
- {
302
- path: "agent.evaluations.topK",
303
- name: "topK",
304
- type: "number",
305
- default: 5,
306
- description: "Default number of ranked search results evaluated per task."
307
- },
308
- {
309
- path: "agent.evaluations.surface",
310
- name: "surface",
311
- type: "\"mcp-context\" | \"configured-search\" | \"ask-ai-context\"",
312
- default: "mcp-context",
313
- description: "Retrieval/context surface measured by the golden task suite."
314
- },
315
- {
316
- path: "agent.evaluations.allowNetwork",
317
- name: "allowNetwork",
318
- type: "boolean",
319
- default: false,
320
- description: "Allow external search, HTTP answers, and explicit executable-example verification during evaluation."
321
- },
322
- {
323
- path: "agent.evaluations.searchTimeoutMs",
324
- name: "searchTimeoutMs",
325
- type: "number",
326
- default: 3e4,
327
- description: "Per-task configured search and Ask AI retrieval timeout in milliseconds."
328
- },
329
- {
330
- path: "agent.evaluations.answer",
331
- name: "answer",
332
- type: "DocsAgentEvaluationAnswerProvider",
333
- description: "Opt-in callback or HTTP provider used to evaluate actual generated answers and citations.",
334
- children: [
335
- {
336
- path: "agent.evaluations.answer.provider",
337
- name: "provider",
338
- type: "\"callback\" | \"http\"",
339
- description: "Answer evaluation provider kind."
340
- },
341
- {
342
- path: "agent.evaluations.answer.run",
343
- name: "run",
344
- type: "DocsAgentEvaluationAnswerRunner",
345
- description: "Callback invoked with retrieved context and source references."
346
- },
347
- {
348
- path: "agent.evaluations.answer.endpoint",
349
- name: "endpoint",
350
- type: "string",
351
- description: "HTTP endpoint used by the opt-in HTTP answer provider."
352
- },
353
- {
354
- path: "agent.evaluations.answer.headers",
355
- name: "headers",
356
- type: "Record<string, string>",
357
- description: "Optional HTTP request headers; values are never reported.",
358
- children: [{
359
- path: "agent.evaluations.answer.headers.*",
360
- name: "header",
240
+ description: "One workspace-contained Agent Skill path."
241
+ }]
242
+ },
243
+ {
244
+ path: "agent.a2a",
245
+ name: "a2a",
246
+ type: "DocsAgentA2AConfig",
247
+ description: "Opt-in Agent Card metadata for a separately implemented real A2A service.",
248
+ children: [
249
+ {
250
+ path: "agent.a2a.interfaceUrl",
251
+ name: "interfaceUrl",
252
+ type: "string",
253
+ description: "HTTP(S) URL of the real A2A interface."
254
+ },
255
+ {
256
+ path: "agent.a2a.name",
257
+ name: "name",
258
+ type: "string",
259
+ description: "Public A2A agent name."
260
+ },
261
+ {
262
+ path: "agent.a2a.description",
263
+ name: "description",
264
+ type: "string",
265
+ description: "Public A2A agent description."
266
+ },
267
+ {
268
+ path: "agent.a2a.documentationUrl",
269
+ name: "documentationUrl",
270
+ type: "string",
271
+ description: "HTTP(S) documentation URL for the A2A service."
272
+ },
273
+ {
274
+ path: "agent.a2a.provider.organization",
275
+ name: "organization",
276
+ type: "string",
277
+ description: "A2A service provider organization."
278
+ },
279
+ {
280
+ path: "agent.a2a.provider.url",
281
+ name: "url",
282
+ type: "string",
283
+ description: "HTTP(S) provider URL."
284
+ },
285
+ {
286
+ path: "agent.a2a.version",
287
+ name: "version",
288
+ type: "string",
289
+ default: "1.0.0",
290
+ description: "A2A service version advertised by the card."
291
+ },
292
+ {
293
+ path: "agent.a2a.protocolVersion",
294
+ name: "protocolVersion",
295
+ type: "string",
296
+ default: "0.3",
297
+ description: "A2A protocol version implemented by the configured interface."
298
+ },
299
+ {
300
+ path: "agent.a2a.protocolBinding",
301
+ name: "protocolBinding",
302
+ type: "string",
303
+ default: "HTTP+JSON",
304
+ description: "A2A transport binding implemented by the configured interface."
305
+ }
306
+ ]
307
+ },
308
+ {
309
+ path: "agent.compact",
310
+ name: "compact",
311
+ type: "DocsAgentCompactConfig",
312
+ description: "Defaults for generated agent.md compaction.",
313
+ children: [
314
+ {
315
+ path: "agent.compact.apiKey",
316
+ name: "apiKey",
317
+ type: "string",
318
+ description: "Direct compaction provider API key; prefer apiKeyEnv."
319
+ },
320
+ {
321
+ path: "agent.compact.apiKeyEnv",
322
+ name: "apiKeyEnv",
323
+ type: "string",
324
+ description: "Environment variable containing the compaction provider API key."
325
+ },
326
+ {
327
+ path: "agent.compact.baseUrl",
328
+ name: "baseUrl",
329
+ type: "string",
330
+ description: "Compaction provider base URL."
331
+ },
332
+ {
333
+ path: "agent.compact.model",
334
+ name: "model",
335
+ type: "string",
336
+ description: "Compaction model identifier."
337
+ },
338
+ {
339
+ path: "agent.compact.aggressiveness",
340
+ name: "aggressiveness",
341
+ type: "number",
342
+ default: .3,
343
+ description: "Compression aggressiveness from 0 to 1."
344
+ },
345
+ {
346
+ path: "agent.compact.maxOutputTokens",
347
+ name: "maxOutputTokens",
348
+ type: "number",
349
+ description: "Upper output token target."
350
+ },
351
+ {
352
+ path: "agent.compact.minOutputTokens",
353
+ name: "minOutputTokens",
354
+ type: "number",
355
+ description: "Lower output token target."
356
+ },
357
+ {
358
+ path: "agent.compact.protectJson",
359
+ name: "protectJson",
360
+ type: "boolean",
361
+ description: "Preserve JSON objects during compaction when supported."
362
+ }
363
+ ]
364
+ },
365
+ {
366
+ path: "agent.evaluations",
367
+ name: "evaluations",
368
+ type: "boolean | DocsAgentEvaluationsConfig",
369
+ description: "Offline-by-default golden tasks for retrieval, citation, version, example, answer, and budget evaluation, with explicit external-provider and execution opt-ins.",
370
+ children: [
371
+ {
372
+ path: "agent.evaluations.enabled",
373
+ name: "enabled",
374
+ type: "boolean",
375
+ default: true,
376
+ description: "Enable configured golden-task evaluation."
377
+ },
378
+ {
379
+ path: "agent.evaluations.tokenBudget",
380
+ name: "tokenBudget",
381
+ type: "number",
382
+ default: 4e3,
383
+ description: "Default hard UTF-8 context-byte ceiling for golden tasks."
384
+ },
385
+ {
386
+ path: "agent.evaluations.topK",
387
+ name: "topK",
388
+ type: "number",
389
+ default: 5,
390
+ description: "Default number of ranked search results evaluated per task."
391
+ },
392
+ {
393
+ path: "agent.evaluations.surface",
394
+ name: "surface",
395
+ type: "\"mcp-context\" | \"configured-search\" | \"ask-ai-context\"",
396
+ default: "mcp-context",
397
+ description: "Retrieval/context surface measured by the golden task suite."
398
+ },
399
+ {
400
+ path: "agent.evaluations.allowNetwork",
401
+ name: "allowNetwork",
402
+ type: "boolean",
403
+ default: false,
404
+ description: "Allow external search, HTTP answers, and explicit executable-example verification during evaluation."
405
+ },
406
+ {
407
+ path: "agent.evaluations.searchTimeoutMs",
408
+ name: "searchTimeoutMs",
409
+ type: "number",
410
+ default: 3e4,
411
+ description: "Per-task configured search and Ask AI retrieval timeout in milliseconds."
412
+ },
413
+ {
414
+ path: "agent.evaluations.answer",
415
+ name: "answer",
416
+ type: "DocsAgentEvaluationAnswerProvider",
417
+ description: "Opt-in callback or HTTP provider used to evaluate actual generated answers and citations.",
418
+ children: [
419
+ {
420
+ path: "agent.evaluations.answer.provider",
421
+ name: "provider",
422
+ type: "\"callback\" | \"http\"",
423
+ description: "Answer evaluation provider kind."
424
+ },
425
+ {
426
+ path: "agent.evaluations.answer.run",
427
+ name: "run",
428
+ type: "DocsAgentEvaluationAnswerRunner",
429
+ description: "Callback invoked with retrieved context and source references."
430
+ },
431
+ {
432
+ path: "agent.evaluations.answer.endpoint",
433
+ name: "endpoint",
361
434
  type: "string",
362
- description: "One custom HTTP header value."
363
- }]
364
- },
365
- {
366
- path: "agent.evaluations.answer.timeoutMs",
367
- name: "timeoutMs",
368
- type: "number",
369
- default: 3e4,
370
- description: "Callback or HTTP answer provider timeout in milliseconds."
371
- }
372
- ]
373
- },
374
- {
375
- path: "agent.evaluations.tasks",
376
- name: "tasks",
377
- type: "DocsAgentGoldenTask[]",
378
- description: "Golden task fixtures evaluated by docs doctor and docs review; the default MCP context surface runs offline.",
379
- children: [
380
- {
381
- path: "agent.evaluations.tasks[]",
382
- name: "task",
383
- type: "DocsAgentGoldenTask",
384
- description: "One golden task entry."
385
- },
386
- {
387
- path: "agent.evaluations.tasks[].id",
388
- name: "id",
389
- type: "string",
390
- description: "Stable task identifier shown in diagnostics."
391
- },
392
- {
393
- path: "agent.evaluations.tasks[].query",
394
- name: "query",
395
- type: "string",
396
- description: "User-shaped retrieval query."
397
- },
398
- {
399
- path: "agent.evaluations.tasks[].tokenBudget",
400
- name: "tokenBudget",
401
- type: "number",
402
- description: "Per-task UTF-8 context-byte ceiling override."
403
- },
404
- {
405
- path: "agent.evaluations.tasks[].topK",
406
- name: "topK",
407
- type: "number",
408
- description: "Per-task ranked retrieval depth override."
409
- },
410
- {
411
- path: "agent.evaluations.tasks[].surface",
412
- name: "surface",
413
- type: "\"mcp-context\" | \"configured-search\" | \"ask-ai-context\"",
414
- description: "Per-task evaluation surface override."
415
- },
416
- {
417
- path: "agent.evaluations.tasks[].filters",
418
- name: "filters",
419
- type: "DocsAgentGoldenTaskFilters",
420
- description: "Framework, version, and locale retrieval scope.",
421
- children: [
422
- {
423
- path: "agent.evaluations.tasks[].filters.framework",
424
- name: "framework",
425
- type: "string",
426
- description: "Required framework, such as nextjs or astro."
427
- },
428
- {
429
- path: "agent.evaluations.tasks[].filters.version",
430
- name: "version",
431
- type: "string",
432
- description: "Exact version requested by the task."
433
- },
434
- {
435
- path: "agent.evaluations.tasks[].filters.locale",
436
- name: "locale",
435
+ description: "HTTP endpoint used by the opt-in HTTP answer provider."
436
+ },
437
+ {
438
+ path: "agent.evaluations.answer.headers",
439
+ name: "headers",
440
+ type: "Record<string, string>",
441
+ description: "Optional HTTP request headers; values are never reported.",
442
+ children: [{
443
+ path: "agent.evaluations.answer.headers.*",
444
+ name: "header",
437
445
  type: "string",
438
- description: "Required locale."
439
- }
440
- ]
441
- },
442
- {
443
- path: "agent.evaluations.tasks[].expect",
444
- name: "expect",
445
- type: "DocsAgentGoldenTaskExpectation",
446
- description: "Evaluator-only source, rank, citation, scope, answer, example, and budget expectations.",
447
- children: [
448
- {
449
- path: "agent.evaluations.tasks[].expect.relevantSources",
450
- name: "relevantSources",
451
- type: "string[]",
452
- description: "Canonical page or section URLs that should answer the task."
453
- },
454
- {
455
- path: "agent.evaluations.tasks[].expect.allowedSources",
456
- name: "allowedSources",
457
- type: "string[]",
458
- description: "Additional legitimate citations that do not reduce precision."
459
- },
460
- {
461
- path: "agent.evaluations.tasks[].expect.forbiddenSources",
462
- name: "forbiddenSources",
463
- type: "string[]",
464
- description: "Sources that must not be retrieved or cited."
465
- },
466
- {
467
- path: "agent.evaluations.tasks[].expect.requiredCitations",
468
- name: "requiredCitations",
469
- type: "string[]",
470
- description: "Citations that must appear; defaults to relevantSources."
471
- },
472
- {
473
- path: "agent.evaluations.tasks[].expect.minRecallAtK",
474
- name: "minRecallAtK",
475
- type: "number",
476
- default: 1,
477
- description: "Minimum relevant-source recall in the top K results."
478
- },
479
- {
480
- path: "agent.evaluations.tasks[].expect.maxFirstRelevantRank",
481
- name: "maxFirstRelevantRank",
482
- type: "number",
483
- description: "Maximum acceptable rank of the first relevant source."
484
- },
485
- {
486
- path: "agent.evaluations.tasks[].expect.minUsefulByteRatio",
487
- name: "minUsefulByteRatio",
488
- type: "number",
489
- description: "Minimum share of context bytes supplied by relevant sources."
490
- },
491
- {
492
- path: "agent.evaluations.tasks[].expect.scope",
493
- name: "scope",
494
- type: "DocsAgentGoldenTaskFilters",
495
- description: "Framework, version, and locale assertions checked against returned sources without pre-filtering retrieval.",
496
- children: [
497
- {
498
- path: "agent.evaluations.tasks[].expect.scope.framework",
499
- name: "framework",
500
- type: "string",
501
- description: "Framework the returned sources must select."
502
- },
503
- {
504
- path: "agent.evaluations.tasks[].expect.scope.version",
505
- name: "version",
506
- type: "string",
507
- description: "Version the returned sources must select."
508
- },
509
- {
510
- path: "agent.evaluations.tasks[].expect.scope.locale",
511
- name: "locale",
512
- type: "string",
513
- description: "Locale the returned sources must select."
514
- }
515
- ]
516
- },
517
- {
518
- path: "agent.evaluations.tasks[].expect.answer",
519
- name: "answer",
520
- type: "DocsAgentGoldenAnswerExpectation",
521
- description: "Required answer text and citation assertions for an explicitly configured answer provider.",
522
- children: [
523
- {
524
- path: "agent.evaluations.tasks[].expect.answer.includes",
525
- name: "includes",
526
- type: "string[]",
527
- description: "Literal fragments required in the actual answer."
528
- },
529
- {
530
- path: "agent.evaluations.tasks[].expect.answer.excludes",
531
- name: "excludes",
532
- type: "string[]",
533
- description: "Literal fragments forbidden in the actual answer."
534
- },
535
- {
536
- path: "agent.evaluations.tasks[].expect.answer.requiredCitations",
537
- name: "requiredCitations",
538
- type: "string[]",
539
- description: "Citations required in the actual answer."
540
- },
541
- {
542
- path: "agent.evaluations.tasks[].expect.answer.allowedCitations",
543
- name: "allowedCitations",
544
- type: "string[]",
545
- description: "Additional valid actual-answer citations."
546
- },
547
- {
548
- path: "agent.evaluations.tasks[].expect.answer.forbiddenCitations",
549
- name: "forbiddenCitations",
550
- type: "string[]",
551
- description: "Citations forbidden in the actual answer."
552
- }
553
- ]
554
- },
555
- {
556
- path: "agent.evaluations.tasks[].expect.examples",
557
- name: "examples",
558
- type: "DocsAgentGoldenExpectedExample[]",
559
- description: "Runnable examples that must be present in returned context.",
560
- children: [
561
- {
562
- path: "agent.evaluations.tasks[].expect.examples[]",
563
- name: "example",
564
- type: "DocsAgentGoldenExpectedExample",
565
- description: "One expected code example."
566
- },
567
- {
568
- path: "agent.evaluations.tasks[].expect.examples[].source",
569
- name: "source",
570
- type: "string",
571
- description: "Canonical source URL containing the example."
572
- },
573
- {
574
- path: "agent.evaluations.tasks[].expect.examples[].language",
575
- name: "language",
576
- type: "string",
577
- description: "Expected code-fence language."
578
- },
579
- {
580
- path: "agent.evaluations.tasks[].expect.examples[].framework",
581
- name: "framework",
582
- type: "string",
583
- description: "Expected framework metadata."
584
- },
585
- {
586
- path: "agent.evaluations.tasks[].expect.examples[].packageManager",
587
- name: "packageManager",
588
- type: "string",
589
- description: "Expected package-manager metadata."
590
- },
591
- {
592
- path: "agent.evaluations.tasks[].expect.examples[].title",
593
- name: "title",
594
- type: "string",
595
- description: "Expected code-fence title metadata."
596
- },
597
- {
598
- path: "agent.evaluations.tasks[].expect.examples[].runnable",
599
- name: "runnable",
600
- type: "boolean",
601
- default: true,
602
- description: "Whether the example must be marked runnable."
603
- },
604
- {
605
- path: "agent.evaluations.tasks[].expect.examples[].includes",
606
- name: "includes",
607
- type: "string[]",
608
- description: "Literal code fragments that must appear."
609
- },
610
- {
611
- path: "agent.evaluations.tasks[].expect.examples[].verification",
612
- name: "verification",
613
- type: "\"present\" | \"syntax\" | \"execute\"",
614
- description: "Required verification strength; defaults to present when runnable is false and syntax otherwise. Runtime execution is always explicit."
615
- }
616
- ]
617
- }
618
- ]
619
- }
620
- ]
621
- }
622
- ]
623
- }]
446
+ description: "One custom HTTP header value."
447
+ }]
448
+ },
449
+ {
450
+ path: "agent.evaluations.answer.timeoutMs",
451
+ name: "timeoutMs",
452
+ type: "number",
453
+ default: 3e4,
454
+ description: "Callback or HTTP answer provider timeout in milliseconds."
455
+ }
456
+ ]
457
+ },
458
+ {
459
+ path: "agent.evaluations.tasks",
460
+ name: "tasks",
461
+ type: "DocsAgentGoldenTask[]",
462
+ description: "Golden task fixtures evaluated by docs doctor and docs review; the default MCP context surface runs offline.",
463
+ children: [
464
+ {
465
+ path: "agent.evaluations.tasks[]",
466
+ name: "task",
467
+ type: "DocsAgentGoldenTask",
468
+ description: "One golden task entry."
469
+ },
470
+ {
471
+ path: "agent.evaluations.tasks[].id",
472
+ name: "id",
473
+ type: "string",
474
+ description: "Stable task identifier shown in diagnostics."
475
+ },
476
+ {
477
+ path: "agent.evaluations.tasks[].query",
478
+ name: "query",
479
+ type: "string",
480
+ description: "User-shaped retrieval query."
481
+ },
482
+ {
483
+ path: "agent.evaluations.tasks[].tokenBudget",
484
+ name: "tokenBudget",
485
+ type: "number",
486
+ description: "Per-task UTF-8 context-byte ceiling override."
487
+ },
488
+ {
489
+ path: "agent.evaluations.tasks[].topK",
490
+ name: "topK",
491
+ type: "number",
492
+ description: "Per-task ranked retrieval depth override."
493
+ },
494
+ {
495
+ path: "agent.evaluations.tasks[].surface",
496
+ name: "surface",
497
+ type: "\"mcp-context\" | \"configured-search\" | \"ask-ai-context\"",
498
+ description: "Per-task evaluation surface override."
499
+ },
500
+ {
501
+ path: "agent.evaluations.tasks[].filters",
502
+ name: "filters",
503
+ type: "DocsAgentGoldenTaskFilters",
504
+ description: "Framework, version, and locale retrieval scope.",
505
+ children: [
506
+ {
507
+ path: "agent.evaluations.tasks[].filters.framework",
508
+ name: "framework",
509
+ type: "string",
510
+ description: "Required framework, such as nextjs or astro."
511
+ },
512
+ {
513
+ path: "agent.evaluations.tasks[].filters.version",
514
+ name: "version",
515
+ type: "string",
516
+ description: "Exact version requested by the task."
517
+ },
518
+ {
519
+ path: "agent.evaluations.tasks[].filters.locale",
520
+ name: "locale",
521
+ type: "string",
522
+ description: "Required locale."
523
+ }
524
+ ]
525
+ },
526
+ {
527
+ path: "agent.evaluations.tasks[].expect",
528
+ name: "expect",
529
+ type: "DocsAgentGoldenTaskExpectation",
530
+ description: "Evaluator-only source, rank, citation, scope, answer, example, and budget expectations.",
531
+ children: [
532
+ {
533
+ path: "agent.evaluations.tasks[].expect.relevantSources",
534
+ name: "relevantSources",
535
+ type: "string[]",
536
+ description: "Canonical page or section URLs that should answer the task."
537
+ },
538
+ {
539
+ path: "agent.evaluations.tasks[].expect.allowedSources",
540
+ name: "allowedSources",
541
+ type: "string[]",
542
+ description: "Additional legitimate citations that do not reduce precision."
543
+ },
544
+ {
545
+ path: "agent.evaluations.tasks[].expect.forbiddenSources",
546
+ name: "forbiddenSources",
547
+ type: "string[]",
548
+ description: "Sources that must not be retrieved or cited."
549
+ },
550
+ {
551
+ path: "agent.evaluations.tasks[].expect.requiredCitations",
552
+ name: "requiredCitations",
553
+ type: "string[]",
554
+ description: "Citations that must appear; defaults to relevantSources."
555
+ },
556
+ {
557
+ path: "agent.evaluations.tasks[].expect.minRecallAtK",
558
+ name: "minRecallAtK",
559
+ type: "number",
560
+ default: 1,
561
+ description: "Minimum relevant-source recall in the top K results."
562
+ },
563
+ {
564
+ path: "agent.evaluations.tasks[].expect.maxFirstRelevantRank",
565
+ name: "maxFirstRelevantRank",
566
+ type: "number",
567
+ description: "Maximum acceptable rank of the first relevant source."
568
+ },
569
+ {
570
+ path: "agent.evaluations.tasks[].expect.minUsefulByteRatio",
571
+ name: "minUsefulByteRatio",
572
+ type: "number",
573
+ description: "Minimum share of context bytes supplied by relevant sources."
574
+ },
575
+ {
576
+ path: "agent.evaluations.tasks[].expect.scope",
577
+ name: "scope",
578
+ type: "DocsAgentGoldenTaskFilters",
579
+ description: "Framework, version, and locale assertions checked against returned sources without pre-filtering retrieval.",
580
+ children: [
581
+ {
582
+ path: "agent.evaluations.tasks[].expect.scope.framework",
583
+ name: "framework",
584
+ type: "string",
585
+ description: "Framework the returned sources must select."
586
+ },
587
+ {
588
+ path: "agent.evaluations.tasks[].expect.scope.version",
589
+ name: "version",
590
+ type: "string",
591
+ description: "Version the returned sources must select."
592
+ },
593
+ {
594
+ path: "agent.evaluations.tasks[].expect.scope.locale",
595
+ name: "locale",
596
+ type: "string",
597
+ description: "Locale the returned sources must select."
598
+ }
599
+ ]
600
+ },
601
+ {
602
+ path: "agent.evaluations.tasks[].expect.answer",
603
+ name: "answer",
604
+ type: "DocsAgentGoldenAnswerExpectation",
605
+ description: "Required answer text and citation assertions for an explicitly configured answer provider.",
606
+ children: [
607
+ {
608
+ path: "agent.evaluations.tasks[].expect.answer.includes",
609
+ name: "includes",
610
+ type: "string[]",
611
+ description: "Literal fragments required in the actual answer."
612
+ },
613
+ {
614
+ path: "agent.evaluations.tasks[].expect.answer.excludes",
615
+ name: "excludes",
616
+ type: "string[]",
617
+ description: "Literal fragments forbidden in the actual answer."
618
+ },
619
+ {
620
+ path: "agent.evaluations.tasks[].expect.answer.requiredCitations",
621
+ name: "requiredCitations",
622
+ type: "string[]",
623
+ description: "Citations required in the actual answer."
624
+ },
625
+ {
626
+ path: "agent.evaluations.tasks[].expect.answer.allowedCitations",
627
+ name: "allowedCitations",
628
+ type: "string[]",
629
+ description: "Additional valid actual-answer citations."
630
+ },
631
+ {
632
+ path: "agent.evaluations.tasks[].expect.answer.forbiddenCitations",
633
+ name: "forbiddenCitations",
634
+ type: "string[]",
635
+ description: "Citations forbidden in the actual answer."
636
+ }
637
+ ]
638
+ },
639
+ {
640
+ path: "agent.evaluations.tasks[].expect.examples",
641
+ name: "examples",
642
+ type: "DocsAgentGoldenExpectedExample[]",
643
+ description: "Runnable examples that must be present in returned context.",
644
+ children: [
645
+ {
646
+ path: "agent.evaluations.tasks[].expect.examples[]",
647
+ name: "example",
648
+ type: "DocsAgentGoldenExpectedExample",
649
+ description: "One expected code example."
650
+ },
651
+ {
652
+ path: "agent.evaluations.tasks[].expect.examples[].source",
653
+ name: "source",
654
+ type: "string",
655
+ description: "Canonical source URL containing the example."
656
+ },
657
+ {
658
+ path: "agent.evaluations.tasks[].expect.examples[].language",
659
+ name: "language",
660
+ type: "string",
661
+ description: "Expected code-fence language."
662
+ },
663
+ {
664
+ path: "agent.evaluations.tasks[].expect.examples[].framework",
665
+ name: "framework",
666
+ type: "string",
667
+ description: "Expected framework metadata."
668
+ },
669
+ {
670
+ path: "agent.evaluations.tasks[].expect.examples[].packageManager",
671
+ name: "packageManager",
672
+ type: "string",
673
+ description: "Expected package-manager metadata."
674
+ },
675
+ {
676
+ path: "agent.evaluations.tasks[].expect.examples[].title",
677
+ name: "title",
678
+ type: "string",
679
+ description: "Expected code-fence title metadata."
680
+ },
681
+ {
682
+ path: "agent.evaluations.tasks[].expect.examples[].runnable",
683
+ name: "runnable",
684
+ type: "boolean",
685
+ default: true,
686
+ description: "Whether the example must be marked runnable."
687
+ },
688
+ {
689
+ path: "agent.evaluations.tasks[].expect.examples[].includes",
690
+ name: "includes",
691
+ type: "string[]",
692
+ description: "Literal code fragments that must appear."
693
+ },
694
+ {
695
+ path: "agent.evaluations.tasks[].expect.examples[].verification",
696
+ name: "verification",
697
+ type: "\"present\" | \"syntax\" | \"execute\"",
698
+ description: "Required verification strength; defaults to present when runnable is false and syntax otherwise. Runtime execution is always explicit."
699
+ }
700
+ ]
701
+ }
702
+ ]
703
+ }
704
+ ]
705
+ }
706
+ ]
707
+ }
708
+ ]
624
709
  },
625
710
  {
626
711
  path: "review",
@@ -966,6 +1051,45 @@ const DOCS_CONFIG_SCHEMA_OPTIONS_TEMPLATE = [
966
1051
  default: "public (callback omitted)",
967
1052
  description: "Opt-in HTTP authentication callback. Return a principal to continue, null for 401, or a Response to control the rejection."
968
1053
  },
1054
+ {
1055
+ path: "mcp.security.protectedResource",
1056
+ name: "protectedResource",
1057
+ type: "DocsMcpProtectedResourceConfig",
1058
+ description: "Opt-in RFC 9728 OAuth protected-resource metadata and endpoint-wide scope enforcement. Active only with authenticate.",
1059
+ children: [
1060
+ {
1061
+ path: "mcp.security.protectedResource.authorizationServers",
1062
+ name: "authorizationServers",
1063
+ type: "string[]",
1064
+ description: "One or more HTTPS OAuth issuer URLs without query or fragment; loopback HTTP is accepted for development."
1065
+ },
1066
+ {
1067
+ path: "mcp.security.protectedResource.scopesSupported",
1068
+ name: "scopesSupported",
1069
+ type: "string[]",
1070
+ description: "OAuth scopes advertised through RFC 9728 scopes_supported metadata."
1071
+ },
1072
+ {
1073
+ path: "mcp.security.protectedResource.requiredScopes",
1074
+ name: "requiredScopes",
1075
+ type: "string[]",
1076
+ description: "Scopes required on every principal returned by authenticate; missing scopes receive a challenged 403."
1077
+ },
1078
+ {
1079
+ path: "mcp.security.protectedResource.resourceName",
1080
+ name: "resourceName",
1081
+ type: "string",
1082
+ default: "resolved MCP server name",
1083
+ description: "Human-readable protected-resource name shown during authorization."
1084
+ },
1085
+ {
1086
+ path: "mcp.security.protectedResource.resourceDocumentation",
1087
+ name: "resourceDocumentation",
1088
+ type: "string",
1089
+ description: "Absolute HTTP(S) URL with human-readable authentication guidance."
1090
+ }
1091
+ ]
1092
+ },
969
1093
  {
970
1094
  path: "mcp.security.maxBodyBytes",
971
1095
  name: "maxBodyBytes",
@@ -1199,13 +1323,18 @@ export default defineDocs({
1199
1323
  });`
1200
1324
  },
1201
1325
  {
1202
- title: "Opt-in MCP authentication",
1326
+ title: "Opt-in OAuth MCP authentication",
1203
1327
  code: `export default defineDocs({
1204
1328
  mcp: {
1205
1329
  security: {
1330
+ protectedResource: {
1331
+ authorizationServers: ["https://auth.example.com"],
1332
+ scopesSupported: ["docs:read"],
1333
+ requiredScopes: ["docs:read"],
1334
+ },
1206
1335
  async authenticate({ request }) {
1207
1336
  const user = await authenticateRequest(request);
1208
- return user ? { id: user.id, scopes: ["docs:read"] } : null;
1337
+ return user ? { id: user.id, scopes: user.scopes } : null;
1209
1338
  },
1210
1339
  },
1211
1340
  },
@@ -1519,9 +1648,7 @@ const contextOutputSchema = z.object({
1519
1648
  sources: z.array(contextSourceOutputSchema)
1520
1649
  });
1521
1650
  function normalizeDocsMcpRoute(route) {
1522
- if (!route || route.trim().length === 0) return DEFAULT_MCP_ROUTE;
1523
- const normalized = `/${route}`.replace(/\/+/g, "/");
1524
- return normalized !== "/" ? normalized.replace(/\/+$/, "") : DEFAULT_MCP_ROUTE;
1651
+ return normalizeDocsMcpEndpointPath(route);
1525
1652
  }
1526
1653
  function resolveDocsMcpConfig(mcp, defaults = {}) {
1527
1654
  if (mcp === false) return {
@@ -1570,10 +1697,52 @@ function resolveDocsMcpSecurityConfig(security) {
1570
1697
  return {
1571
1698
  allowedOrigins: security?.allowedOrigins ?? "same-origin",
1572
1699
  authenticate: security?.authenticate,
1700
+ protectedResource: resolveDocsMcpProtectedResourceConfig(security?.protectedResource),
1573
1701
  maxBodyBytes,
1574
1702
  cors: resolveDocsMcpCorsConfig(security?.cors)
1575
1703
  };
1576
1704
  }
1705
+ function resolveDocsMcpProtectedResourceConfig(config) {
1706
+ if (!config || typeof config !== "object") return void 0;
1707
+ const configuredAuthorizationServers = config.authorizationServers;
1708
+ if (!Array.isArray(configuredAuthorizationServers) || configuredAuthorizationServers.length === 0 || configuredAuthorizationServers.some((value) => typeof value !== "string" || value.trim().length === 0)) throw new TypeError("mcp.security.protectedResource.authorizationServers must contain at least one authorization server issuer URL.");
1709
+ const authorizationServerCandidates = normalizeMcpStringList(configuredAuthorizationServers);
1710
+ const authorizationServers = normalizeDocsMcpAuthorizationServerUrls(authorizationServerCandidates);
1711
+ if (authorizationServers.length !== authorizationServerCandidates.length) throw new TypeError("mcp.security.protectedResource.authorizationServers must use HTTPS issuer URLs without query strings or fragments; HTTP is allowed only for loopback development.");
1712
+ const resourceName = normalizeMcpOptionalString(config.resourceName);
1713
+ const resourceDocumentation = normalizeMcpOptionalHttpUrl(config.resourceDocumentation);
1714
+ if (config.resourceDocumentation !== void 0 && !resourceDocumentation) throw new TypeError("mcp.security.protectedResource.resourceDocumentation must be an absolute HTTP or HTTPS URL.");
1715
+ return {
1716
+ authorizationServers,
1717
+ scopesSupported: normalizeMcpScopeList(config.scopesSupported, "mcp.security.protectedResource.scopesSupported"),
1718
+ requiredScopes: normalizeMcpScopeList(config.requiredScopes, "mcp.security.protectedResource.requiredScopes"),
1719
+ ...resourceName ? { resourceName } : {},
1720
+ ...resourceDocumentation ? { resourceDocumentation } : {}
1721
+ };
1722
+ }
1723
+ function normalizeMcpStringList(values) {
1724
+ if (!Array.isArray(values)) return [];
1725
+ return Array.from(new Set(values.filter((value) => typeof value === "string").map((value) => value.trim()).filter(Boolean)));
1726
+ }
1727
+ function normalizeMcpScopeList(values, path) {
1728
+ if (values === void 0) return [];
1729
+ if (!Array.isArray(values) || values.some((value) => typeof value !== "string" || !isDocsMcpOAuthScopeToken(value.trim()))) throw new TypeError(`${path} must contain valid OAuth scope tokens.`);
1730
+ return normalizeMcpStringList(values);
1731
+ }
1732
+ function normalizeMcpOptionalString(value) {
1733
+ if (typeof value !== "string") return void 0;
1734
+ return value.trim() || void 0;
1735
+ }
1736
+ function normalizeMcpOptionalHttpUrl(value) {
1737
+ const normalized = normalizeMcpOptionalString(value);
1738
+ if (!normalized) return void 0;
1739
+ try {
1740
+ const url = new URL(normalized);
1741
+ return (url.protocol === "https:" || url.protocol === "http:") && !url.username && !url.password ? normalized : void 0;
1742
+ } catch {
1743
+ return;
1744
+ }
1745
+ }
1577
1746
  function resolveDocsMcpCorsConfig(cors) {
1578
1747
  const config = cors && typeof cors === "object" ? cors : {};
1579
1748
  const configuredMaxAge = config.maxAgeSeconds;
@@ -1661,6 +1830,9 @@ async function createDocsMcpServer(options) {
1661
1830
  function getSourceNavigation(locale) {
1662
1831
  return options.source.getNavigation(locale, options.requestContext);
1663
1832
  }
1833
+ function getSourceSkills() {
1834
+ return options.source.getSkills?.(options.requestContext) ?? [];
1835
+ }
1664
1836
  function trackMcpTool(tool, values) {
1665
1837
  emitDocsTelemetryMcpToolEvent(telemetryConfig, {
1666
1838
  framework: telemetryFramework,
@@ -1672,6 +1844,7 @@ async function createDocsMcpServer(options) {
1672
1844
  }
1673
1845
  const defaultPages = dedupePages(await getSourcePages());
1674
1846
  const defaultTree = await getSourceNavigation();
1847
+ const defaultSkills = await getSourceSkills();
1675
1848
  server.registerResource("docs-navigation", "docs://navigation", {
1676
1849
  title: "Docs Navigation",
1677
1850
  description: "Structured navigation tree for the documentation site.",
@@ -1693,6 +1866,23 @@ async function createDocsMcpServer(options) {
1693
1866
  text: renderPageDocument(page)
1694
1867
  }] }));
1695
1868
  }
1869
+ for (const skill of defaultSkills) for (const file of skill.files) {
1870
+ const encodedPath = file.path.split("/").map((segment) => encodeURIComponent(segment)).join("/");
1871
+ const resourceUri = `docs://skills/${encodeURIComponent(skill.name)}/${encodedPath}`;
1872
+ server.registerResource(`skill-${encodeURIComponent(skill.name)}-${Buffer.from(file.path).toString("base64url")}`, resourceUri, {
1873
+ title: `${skill.name}: ${file.path}`,
1874
+ description: `${skill.description} (${file.digest})`,
1875
+ mimeType: file.mediaType
1876
+ }, async () => ({ contents: [typeof file.content === "string" ? {
1877
+ uri: resourceUri,
1878
+ mimeType: file.mediaType,
1879
+ text: file.content
1880
+ } : {
1881
+ uri: resourceUri,
1882
+ mimeType: file.mediaType,
1883
+ blob: Buffer.from(file.content).toString("base64")
1884
+ }] }));
1885
+ }
1696
1886
  if (resolved.tools.listPages) server.registerTool("list_pages", {
1697
1887
  title: "List docs pages",
1698
1888
  description: "List the known documentation pages with titles, slugs, and URLs.",
@@ -2802,12 +2992,26 @@ function createDocsMcpHttpHandler(options) {
2802
2992
  };
2803
2993
  }
2804
2994
  async function handle(request) {
2805
- const url = new URL(request.url);
2995
+ const originalUrl = new URL(request.url);
2806
2996
  const method = request.method.toUpperCase();
2807
2997
  const security = resolved.security ?? resolveDocsMcpSecurityConfig();
2998
+ const metadataLocation = resolveDocsMcpProtectedResourceMetadataLocation(request, resolved.route);
2999
+ if (metadataLocation || isDocsMcpProtectedResourceMetadataPath(originalUrl.pathname)) {
3000
+ if (security.authenticate && security.protectedResource && metadataLocation && !isAllowedMcpProtectedResourceUrl(metadataLocation.resourceUrl)) return createMcpHttpSecurityErrorResponse(400, "Protected MCP requires HTTPS; HTTP is allowed only for loopback development");
3001
+ return createDocsMcpProtectedResourceMetadataResponse({
3002
+ request,
3003
+ location: metadataLocation,
3004
+ config: security.authenticate ? security.protectedResource : void 0,
3005
+ defaultResourceName: resolved.name
3006
+ });
3007
+ }
3008
+ if (security.authenticate && security.protectedResource && !isDocsMcpResourcePath(originalUrl.pathname, resolved.route)) return createJsonErrorResponse(404, "Not Found");
3009
+ const resourceLocation = resolveDocsMcpResourceLocation(request, resolved.route);
3010
+ if (security.authenticate && security.protectedResource && !isAllowedMcpProtectedResourceUrl(resourceLocation.resourceUrl)) return createMcpHttpSecurityErrorResponse(400, "Protected MCP requires HTTPS; HTTP is allowed only for loopback development");
2808
3011
  const prepared = await prepareDocsMcpHttpRequest(request, security.maxBodyBytes);
2809
3012
  if (prepared.status === "too-large") return createMcpRequestTooLargeResponse(security.maxBodyBytes);
2810
3013
  request = prepared.request;
3014
+ const url = new URL(request.url);
2811
3015
  let originAllowed;
2812
3016
  try {
2813
3017
  originAllowed = await isDocsMcpOriginAllowed(request.clone(), security.allowedOrigins);
@@ -2823,14 +3027,16 @@ function createDocsMcpHttpHandler(options) {
2823
3027
  try {
2824
3028
  authentication = await security.authenticate({
2825
3029
  request: request.clone(),
2826
- pathname: url.pathname
3030
+ pathname: resourceLocation.resourceUrl.pathname,
3031
+ resource: serializeMcpResourceIdentifier(resourceLocation.resourceUrl)
2827
3032
  });
2828
3033
  } catch {
2829
3034
  return withCors(createMcpHttpSecurityErrorResponse(500, "MCP authentication failed"));
2830
3035
  }
2831
3036
  if (authentication instanceof Response) return withCors(authentication);
2832
- if (authentication === null || authentication === void 0) return withCors(createMcpHttpSecurityErrorResponse(401, "Unauthorized"));
3037
+ if (authentication === null || authentication === void 0) return withCors(createMcpUnauthorizedResponse(request, resourceLocation, security.protectedResource));
2833
3038
  if (!isDocsMcpAuthPrincipal(authentication)) return withCors(createMcpHttpSecurityErrorResponse(500, "MCP authentication returned an invalid principal"));
3039
+ if (findMissingMcpScopes(authentication.scopes, security.protectedResource?.requiredScopes).length > 0) return withCors(createMcpInsufficientScopeResponse(resourceLocation, security.protectedResource));
2834
3040
  auth = authentication;
2835
3041
  }
2836
3042
  const sessionId = request.headers.get("mcp-session-id") ?? request.headers.get("Mcp-Session-Id");
@@ -3054,6 +3260,91 @@ function cloneResponseWithHeaders(response, headers) {
3054
3260
  headers
3055
3261
  });
3056
3262
  }
3263
+ function createDocsMcpProtectedResourceMetadataResponse({ request, location, config, defaultResourceName }) {
3264
+ const method = request.method.toUpperCase();
3265
+ const headers = new Headers({
3266
+ "Access-Control-Allow-Origin": "*",
3267
+ "Access-Control-Expose-Headers": "Content-Type",
3268
+ Allow: "GET, HEAD, OPTIONS",
3269
+ "Cache-Control": config && location ? "public, max-age=300" : "no-store",
3270
+ "Content-Type": "application/json",
3271
+ "X-Robots-Tag": "noindex"
3272
+ });
3273
+ if (!config || !location) return new Response(method === "HEAD" ? null : JSON.stringify({ error: "Not Found" }), {
3274
+ status: 404,
3275
+ headers
3276
+ });
3277
+ if (method === "OPTIONS") {
3278
+ headers.set("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS");
3279
+ headers.set("Access-Control-Allow-Headers", "MCP-Protocol-Version");
3280
+ headers.set("Access-Control-Max-Age", "600");
3281
+ return new Response(null, {
3282
+ status: 204,
3283
+ headers
3284
+ });
3285
+ }
3286
+ if (method !== "GET" && method !== "HEAD") return new Response(JSON.stringify({ error: "Method Not Allowed" }), {
3287
+ status: 405,
3288
+ headers
3289
+ });
3290
+ const metadata = {
3291
+ resource: serializeMcpResourceIdentifier(location.resourceUrl),
3292
+ authorization_servers: config.authorizationServers,
3293
+ scopes_supported: config.scopesSupported.length > 0 ? config.scopesSupported : void 0,
3294
+ bearer_methods_supported: ["header"],
3295
+ resource_name: config.resourceName ?? defaultResourceName,
3296
+ resource_documentation: config.resourceDocumentation
3297
+ };
3298
+ return new Response(method === "HEAD" ? null : JSON.stringify(metadata), { headers });
3299
+ }
3300
+ function serializeMcpResourceIdentifier(resourceUrl) {
3301
+ return resourceUrl.pathname === "/" ? resourceUrl.origin : resourceUrl.href;
3302
+ }
3303
+ function isAllowedMcpProtectedResourceUrl(resourceUrl) {
3304
+ if (resourceUrl.protocol === "https:") return true;
3305
+ return resourceUrl.protocol === "http:" && (resourceUrl.hostname === "localhost" || resourceUrl.hostname === "127.0.0.1" || resourceUrl.hostname === "[::1]");
3306
+ }
3307
+ function findMissingMcpScopes(grantedScopes, requiredScopes) {
3308
+ if (!requiredScopes || requiredScopes.length === 0) return [];
3309
+ const granted = new Set(grantedScopes ?? []);
3310
+ return requiredScopes.filter((scope) => !granted.has(scope));
3311
+ }
3312
+ function createMcpUnauthorizedResponse(request, location, config) {
3313
+ if (!config) return createMcpHttpSecurityErrorResponse(401, "Unauthorized");
3314
+ return createMcpOAuthErrorResponse(401, "invalid_token", buildMcpBearerChallenge({
3315
+ error: request.headers.has("authorization") ? "invalid_token" : void 0,
3316
+ resourceMetadata: location.metadataUrl.href,
3317
+ scopes: config.requiredScopes
3318
+ }));
3319
+ }
3320
+ function createMcpInsufficientScopeResponse(location, config) {
3321
+ if (!config) return createMcpHttpSecurityErrorResponse(403, "Forbidden");
3322
+ return createMcpOAuthErrorResponse(403, "insufficient_scope", buildMcpBearerChallenge({
3323
+ error: "insufficient_scope",
3324
+ resourceMetadata: location.metadataUrl.href,
3325
+ scopes: config.requiredScopes
3326
+ }));
3327
+ }
3328
+ function buildMcpBearerChallenge({ error, resourceMetadata, scopes }) {
3329
+ return `Bearer ${[
3330
+ ...error ? [`error=${quoteHttpAuthParameter(error)}`] : [],
3331
+ `resource_metadata=${quoteHttpAuthParameter(resourceMetadata)}`,
3332
+ ...scopes.length > 0 ? [`scope=${quoteHttpAuthParameter(scopes.join(" "))}`] : []
3333
+ ].join(", ")}`;
3334
+ }
3335
+ function quoteHttpAuthParameter(value) {
3336
+ return `"${value.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"")}"`;
3337
+ }
3338
+ function createMcpOAuthErrorResponse(status, error, challenge) {
3339
+ return new Response(JSON.stringify({ error }), {
3340
+ status,
3341
+ headers: {
3342
+ "Content-Type": "application/json",
3343
+ "Cache-Control": "no-store",
3344
+ "WWW-Authenticate": challenge
3345
+ }
3346
+ });
3347
+ }
3057
3348
  function createMcpRequestTooLargeResponse(maxBodyBytes) {
3058
3349
  const response = createJsonRpcErrorResponse({
3059
3350
  status: 413,