zudoku 0.71.10 → 0.73.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/cli.js +17 -9
- package/dist/declarations/lib/plugins/openapi/MCPEndpoint.d.ts +2 -1
- package/docs/configuration/navigation.mdx +294 -9
- package/docs/guides/mcp-servers.md +189 -0
- package/package.json +3 -4
- package/src/lib/authentication/components/OAuthErrorPage.tsx +6 -10
- package/src/lib/authentication/providers/openid.tsx +14 -1
- package/src/lib/components/DeveloperHint.tsx +1 -1
- package/src/lib/components/Layout.tsx +2 -2
- package/src/lib/plugins/openapi/MCPEndpoint.tsx +43 -7
- package/src/lib/plugins/openapi/OperationListItem.tsx +1 -0
- package/src/lib/plugins/openapi/playground/Playground.tsx +3 -2
- package/src/lib/plugins/openapi/schema/SchemaView.tsx +1 -1
- package/src/lib/testing/index.tsx +1 -1
- package/src/lib/util/problemJson.ts +36 -1
- package/src/vite/plugin-markdown-export.ts +7 -6
- package/src/vite/prerender/prerender.ts +11 -2
package/dist/cli/cli.js
CHANGED
|
@@ -3810,7 +3810,7 @@ import {
|
|
|
3810
3810
|
// package.json
|
|
3811
3811
|
var package_default = {
|
|
3812
3812
|
name: "zudoku",
|
|
3813
|
-
version: "0.
|
|
3813
|
+
version: "0.72.0",
|
|
3814
3814
|
type: "module",
|
|
3815
3815
|
sideEffects: [
|
|
3816
3816
|
"**/*.css",
|
|
@@ -3871,7 +3871,6 @@ var package_default = {
|
|
|
3871
3871
|
"./vite": "./src/vite/index.ts",
|
|
3872
3872
|
"./app/*": "./src/app/*",
|
|
3873
3873
|
"./hooks": "./src/lib/hooks/index.ts",
|
|
3874
|
-
"./main.css": "./src/app/main.css",
|
|
3875
3874
|
"./processors/*": "./src/lib/plugins/openapi/processors/*.ts",
|
|
3876
3875
|
"./with-zuplo": "./src/zuplo/with-zuplo.ts",
|
|
3877
3876
|
"./testing": "./src/lib/testing/index.tsx"
|
|
@@ -3882,7 +3881,7 @@ var package_default = {
|
|
|
3882
3881
|
typecheck: "tsc --project tsconfig.app.json --noEmit",
|
|
3883
3882
|
"generate:types": "tsx scripts/generate-types.js && tsx scripts/generate-flat-config.js",
|
|
3884
3883
|
"build:standalone": "vite build --mode standalone --config vite.standalone.config.ts --log-level=error",
|
|
3885
|
-
prepublishOnly: "tsx scripts/generate-publish-exports.ts",
|
|
3884
|
+
prepublishOnly: "tsx scripts/generate-publish-exports.ts && publint .",
|
|
3886
3885
|
postpublish: "git checkout -- package.json",
|
|
3887
3886
|
clean: "rm -rf dist",
|
|
3888
3887
|
codegen: "graphql-codegen --config ./src/codegen.ts"
|
|
@@ -4002,10 +4001,10 @@ var package_default = {
|
|
|
4002
4001
|
vaul: "1.1.2",
|
|
4003
4002
|
vfile: "6.0.3",
|
|
4004
4003
|
vite: "7.3.1",
|
|
4005
|
-
yaml: "2.8.
|
|
4004
|
+
yaml: "2.8.3",
|
|
4006
4005
|
yargs: "18.0.0",
|
|
4007
4006
|
zod: "4.3.6",
|
|
4008
|
-
zustand: "5.0.
|
|
4007
|
+
zustand: "5.0.12"
|
|
4009
4008
|
},
|
|
4010
4009
|
devDependencies: {
|
|
4011
4010
|
"@clerk/clerk-js": "^5.125.3",
|
|
@@ -6606,6 +6605,10 @@ ${markdownContent}`;
|
|
|
6606
6605
|
description: frontmatter.description
|
|
6607
6606
|
};
|
|
6608
6607
|
};
|
|
6608
|
+
var getMarkdownOutputPath = (distDir, routePath) => {
|
|
6609
|
+
const segments = routePath === "/" ? ["index"] : routePath.split("/").filter(Boolean);
|
|
6610
|
+
return `${path14.join(distDir, ...segments)}.md`;
|
|
6611
|
+
};
|
|
6609
6612
|
var viteMarkdownExportPlugin = () => {
|
|
6610
6613
|
let markdownFiles = {};
|
|
6611
6614
|
let markdownFileInfos = [];
|
|
@@ -6699,8 +6702,7 @@ var viteMarkdownExportPlugin = () => {
|
|
|
6699
6702
|
description,
|
|
6700
6703
|
content: finalMarkdown
|
|
6701
6704
|
});
|
|
6702
|
-
const
|
|
6703
|
-
const outputPath = `${path14.join(distDir, ...segments)}.md`;
|
|
6705
|
+
const outputPath = getMarkdownOutputPath(distDir, routePath);
|
|
6704
6706
|
await mkdir2(path14.dirname(outputPath), { recursive: true });
|
|
6705
6707
|
await writeFile2(outputPath, finalMarkdown, "utf-8");
|
|
6706
6708
|
} catch (error) {
|
|
@@ -7896,8 +7898,14 @@ var prerender = async ({
|
|
|
7896
7898
|
}
|
|
7897
7899
|
if (!docsConfig.publishMarkdown) {
|
|
7898
7900
|
await Promise.all(
|
|
7899
|
-
markdownFileInfos.map((info) =>
|
|
7900
|
-
|
|
7901
|
+
markdownFileInfos.map((info) => {
|
|
7902
|
+
const outputPath = getMarkdownOutputPath(distDir, info.routePath);
|
|
7903
|
+
if (!path21.resolve(outputPath).startsWith(path21.resolve(distDir))) {
|
|
7904
|
+
return;
|
|
7905
|
+
}
|
|
7906
|
+
return rm(outputPath).catch(() => {
|
|
7907
|
+
});
|
|
7908
|
+
})
|
|
7901
7909
|
);
|
|
7902
7910
|
}
|
|
7903
7911
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
export declare const MCPEndpoint: ({ serverUrl, summary, data, }: {
|
|
1
|
+
export declare const MCPEndpoint: ({ serverUrl, operationPath, summary, data, }: {
|
|
2
2
|
serverUrl?: string;
|
|
3
|
+
operationPath?: string;
|
|
3
4
|
data?: boolean | Record<string, unknown>;
|
|
4
5
|
summary?: string;
|
|
5
6
|
}) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -11,7 +11,8 @@ import { Book, Code, FileText } from "zudoku/icons";
|
|
|
11
11
|
|
|
12
12
|
Zudoku uses a single `navigation` array to control both the top navigation tabs and the sidebar.
|
|
13
13
|
Items at the root of this array appear as tabs, and nested items build the sidebar tree. Navigation
|
|
14
|
-
entries can be links, document references, categories
|
|
14
|
+
entries can be links, document references, categories, custom pages, separators, sections, or
|
|
15
|
+
filters.
|
|
15
16
|
|
|
16
17
|
## Basic configuration
|
|
17
18
|
|
|
@@ -45,7 +46,6 @@ one of several types. At the simplest level you may only have links and categori
|
|
|
45
46
|
"to": "/api",
|
|
46
47
|
"label": "API Reference",
|
|
47
48
|
"icon": "code",
|
|
48
|
-
"description": "Complete API documentation",
|
|
49
49
|
"badge": {
|
|
50
50
|
"label": "v2.0",
|
|
51
51
|
"color": "blue"
|
|
@@ -58,13 +58,18 @@ one of several types. At the simplest level you may only have links and categori
|
|
|
58
58
|
|
|
59
59
|
## Navigation Items
|
|
60
60
|
|
|
61
|
-
Navigation items can be of these types: `category`, `doc`, `link`,
|
|
61
|
+
Navigation items can be of these types: `category`, `doc`, `link`, `custom-page`, `separator`,
|
|
62
|
+
`section`, or `filter`.
|
|
62
63
|
|
|
63
64
|
- `link`: A direct link to a page or external URL.
|
|
64
65
|
- `category`: A group of links that can be expanded or collapsed.
|
|
65
|
-
- `doc`: A reference to a document by
|
|
66
|
-
- `custom-
|
|
66
|
+
- `doc`: A reference to a document by its file path: `file`.
|
|
67
|
+
- `custom-page`: A custom page that is made of a React component, see
|
|
67
68
|
[Custom Pages](../guides/custom-pages.md)
|
|
69
|
+
- `separator`: A horizontal line to visually divide sidebar items.
|
|
70
|
+
- `section`: A non-interactive heading label to group sidebar items.
|
|
71
|
+
- `filter`: An inline search input that filters navigation items. Multiple filter inputs share the
|
|
72
|
+
same search query.
|
|
68
73
|
|
|
69
74
|
### `type: link`
|
|
70
75
|
|
|
@@ -87,10 +92,10 @@ type NavigationLink = {
|
|
|
87
92
|
to: string;
|
|
88
93
|
label: string;
|
|
89
94
|
icon?: string; // Lucide icon name
|
|
90
|
-
description?: string;
|
|
91
95
|
badge?: {
|
|
92
96
|
label: string;
|
|
93
97
|
color: "green" | "blue" | "yellow" | "red" | "purple" | "indigo" | "gray" | "outline";
|
|
98
|
+
invert?: boolean;
|
|
94
99
|
};
|
|
95
100
|
display?:
|
|
96
101
|
| "auth"
|
|
@@ -106,7 +111,8 @@ type NavigationLink = {
|
|
|
106
111
|
### `type: category`
|
|
107
112
|
|
|
108
113
|
The `category` type groups related items under a collapsible section. The `label` is the displayed
|
|
109
|
-
text, and the `items` array
|
|
114
|
+
text, and the `items` array can contain any navigation item type (documents, links, categories,
|
|
115
|
+
custom pages, separators, sections, and filters).
|
|
110
116
|
|
|
111
117
|
```json
|
|
112
118
|
{
|
|
@@ -131,11 +137,11 @@ text, and the `items` array contains `id`s of documents, links, or other categor
|
|
|
131
137
|
type NavigationCategory = {
|
|
132
138
|
type: "category";
|
|
133
139
|
icon?: string; // Lucide icon name
|
|
134
|
-
items: Array<
|
|
140
|
+
items: Array<NavigationItem>; // any navigation item type, including string shorthands for docs
|
|
135
141
|
label: string;
|
|
136
142
|
collapsible?: boolean;
|
|
137
143
|
collapsed?: boolean;
|
|
138
|
-
link?: string | { type: "doc"; file: string; label?: string };
|
|
144
|
+
link?: string | { type: "doc"; file: string; label?: string; path?: string };
|
|
139
145
|
display?:
|
|
140
146
|
| "auth"
|
|
141
147
|
| "anon"
|
|
@@ -147,6 +153,50 @@ type NavigationCategory = {
|
|
|
147
153
|
|
|
148
154
|
</details>
|
|
149
155
|
|
|
156
|
+
#### Category links
|
|
157
|
+
|
|
158
|
+
A category can have a `link` property that makes the category label itself clickable, navigating to
|
|
159
|
+
a document. This is useful when you want a category that acts as both a group and a landing page.
|
|
160
|
+
|
|
161
|
+
The `link` can be a simple string pointing to a file path, or an object for more control:
|
|
162
|
+
|
|
163
|
+
```tsx title="String shorthand"
|
|
164
|
+
{
|
|
165
|
+
type: "category",
|
|
166
|
+
label: "Configuration",
|
|
167
|
+
link: "docs/configuration/overview",
|
|
168
|
+
items: [
|
|
169
|
+
"docs/configuration/navigation",
|
|
170
|
+
"docs/configuration/site",
|
|
171
|
+
],
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
```tsx title="Object form with custom path"
|
|
176
|
+
{
|
|
177
|
+
type: "category",
|
|
178
|
+
label: "Documentation",
|
|
179
|
+
link: {
|
|
180
|
+
type: "doc",
|
|
181
|
+
file: "home.md",
|
|
182
|
+
path: "/",
|
|
183
|
+
},
|
|
184
|
+
items: [
|
|
185
|
+
"guides/getting-started",
|
|
186
|
+
"guides/advanced",
|
|
187
|
+
],
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
The object form supports these properties:
|
|
192
|
+
|
|
193
|
+
| Property | Type | Description |
|
|
194
|
+
| -------- | -------- | -------------------------------------------------------- |
|
|
195
|
+
| `type` | `"doc"` | Must be `"doc"` |
|
|
196
|
+
| `file` | `string` | Path to the markdown file |
|
|
197
|
+
| `label` | `string` | Override the label (defaults to the document title) |
|
|
198
|
+
| `path` | `string` | Custom URL path (overrides the default file-based route) |
|
|
199
|
+
|
|
150
200
|
### `type: doc`
|
|
151
201
|
|
|
152
202
|
Doc is used to reference markdown files. The `label` is the text that will be displayed, and the
|
|
@@ -173,6 +223,7 @@ type NavigationDoc = {
|
|
|
173
223
|
badge?: {
|
|
174
224
|
label: string;
|
|
175
225
|
color: "green" | "blue" | "yellow" | "red" | "purple" | "indigo" | "gray" | "outline";
|
|
226
|
+
invert?: boolean;
|
|
176
227
|
};
|
|
177
228
|
display?:
|
|
178
229
|
| "auth"
|
|
@@ -223,6 +274,37 @@ Learn more in the [Markdown documentation](/docs/markdown/overview)
|
|
|
223
274
|
The `path` property allows you to customize the URL path for a document. By default, Zudoku uses the
|
|
224
275
|
file path to generate the URL, but you can override this behavior by specifying a custom path.
|
|
225
276
|
|
|
277
|
+
```tsx title="Serving a doc at the root URL"
|
|
278
|
+
{
|
|
279
|
+
type: "doc",
|
|
280
|
+
file: "home.md",
|
|
281
|
+
path: "/",
|
|
282
|
+
label: "Home",
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
```tsx title="Custom slug"
|
|
287
|
+
{
|
|
288
|
+
type: "doc",
|
|
289
|
+
file: "guides/getting-started.md",
|
|
290
|
+
path: "/start-here",
|
|
291
|
+
label: "Start Here",
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
When a file has a custom path, it will only be accessible at that custom path, not at its original
|
|
296
|
+
file-based path. See [Documentation - Custom Paths](/docs/configuration/docs#custom-paths) for more
|
|
297
|
+
details.
|
|
298
|
+
|
|
299
|
+
:::note
|
|
300
|
+
|
|
301
|
+
Avoid naming files `index.md` or `index.mdx` and relying on their default path. Some hosting
|
|
302
|
+
providers (e.g. Vercel) automatically strip `/index` from URLs with a redirect, which can cause
|
|
303
|
+
routing issues. Instead, give files descriptive names and use the `path` property to serve them at
|
|
304
|
+
the desired URL.
|
|
305
|
+
|
|
306
|
+
:::
|
|
307
|
+
|
|
226
308
|
### `type: custom-page`
|
|
227
309
|
|
|
228
310
|
Custom pages allow you to create standalone pages that are not tied to a Markdown document. This is
|
|
@@ -247,6 +329,7 @@ type NavigationCustomPage = {
|
|
|
247
329
|
label?: string;
|
|
248
330
|
element: any;
|
|
249
331
|
icon?: string; // Lucide icon name
|
|
332
|
+
layout?: "default" | "none";
|
|
250
333
|
badge?: {
|
|
251
334
|
label: string;
|
|
252
335
|
color: "green" | "blue" | "yellow" | "red" | "purple" | "indigo" | "gray" | "outline";
|
|
@@ -263,6 +346,120 @@ type NavigationCustomPage = {
|
|
|
263
346
|
|
|
264
347
|
</details>
|
|
265
348
|
|
|
349
|
+
Set `layout: "none"` to render the page without the default Zudoku layout (header, sidebar, footer).
|
|
350
|
+
This is useful for fully custom landing pages.
|
|
351
|
+
|
|
352
|
+
### `type: separator`
|
|
353
|
+
|
|
354
|
+
A visual divider line in the sidebar. Use separators to create visual breaks between groups of
|
|
355
|
+
items.
|
|
356
|
+
|
|
357
|
+
```tsx
|
|
358
|
+
{
|
|
359
|
+
type: "category",
|
|
360
|
+
label: "Documentation",
|
|
361
|
+
items: [
|
|
362
|
+
"guides/getting-started",
|
|
363
|
+
"guides/installation",
|
|
364
|
+
{ type: "separator" },
|
|
365
|
+
"guides/advanced",
|
|
366
|
+
"guides/troubleshooting",
|
|
367
|
+
],
|
|
368
|
+
}
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
<details>
|
|
372
|
+
<summary>**TypeScript type declaration**</summary>
|
|
373
|
+
|
|
374
|
+
```ts
|
|
375
|
+
type NavigationSeparator = {
|
|
376
|
+
type: "separator";
|
|
377
|
+
display?:
|
|
378
|
+
| "auth"
|
|
379
|
+
| "anon"
|
|
380
|
+
| "always"
|
|
381
|
+
| "hide"
|
|
382
|
+
| ((params: { context: ZudokuContext; auth: UseAuthReturn }) => boolean);
|
|
383
|
+
};
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
</details>
|
|
387
|
+
|
|
388
|
+
### `type: section`
|
|
389
|
+
|
|
390
|
+
A non-interactive heading label in the sidebar. Sections are rendered as small uppercase text and
|
|
391
|
+
are useful for labeling groups of items without adding a collapsible wrapper.
|
|
392
|
+
|
|
393
|
+
```tsx
|
|
394
|
+
{
|
|
395
|
+
type: "category",
|
|
396
|
+
label: "Documentation",
|
|
397
|
+
items: [
|
|
398
|
+
{ type: "section", label: "Getting Started" },
|
|
399
|
+
"guides/quickstart",
|
|
400
|
+
"guides/installation",
|
|
401
|
+
{ type: "section", label: "Advanced" },
|
|
402
|
+
"guides/plugins",
|
|
403
|
+
"guides/deployment",
|
|
404
|
+
],
|
|
405
|
+
}
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
<details>
|
|
409
|
+
<summary>**TypeScript type declaration**</summary>
|
|
410
|
+
|
|
411
|
+
```ts
|
|
412
|
+
type NavigationSection = {
|
|
413
|
+
type: "section";
|
|
414
|
+
label: string;
|
|
415
|
+
display?:
|
|
416
|
+
| "auth"
|
|
417
|
+
| "anon"
|
|
418
|
+
| "always"
|
|
419
|
+
| "hide"
|
|
420
|
+
| ((params: { context: ZudokuContext; auth: UseAuthReturn }) => boolean);
|
|
421
|
+
};
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
</details>
|
|
425
|
+
|
|
426
|
+
### `type: filter`
|
|
427
|
+
|
|
428
|
+
An inline search input that updates the shared navigation filter. When the user types, the query is
|
|
429
|
+
applied across the sidebar so that only matching items remain visible.
|
|
430
|
+
|
|
431
|
+
```tsx
|
|
432
|
+
{
|
|
433
|
+
type: "category",
|
|
434
|
+
label: "Documentation",
|
|
435
|
+
items: [
|
|
436
|
+
{ type: "filter", placeholder: "Filter documentation" },
|
|
437
|
+
"guides/getting-started",
|
|
438
|
+
"guides/installation",
|
|
439
|
+
"guides/authentication",
|
|
440
|
+
"guides/deployment",
|
|
441
|
+
],
|
|
442
|
+
}
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
<details>
|
|
446
|
+
<summary>**TypeScript type declaration**</summary>
|
|
447
|
+
|
|
448
|
+
```ts
|
|
449
|
+
type NavigationFilter = {
|
|
450
|
+
type: "filter";
|
|
451
|
+
placeholder?: string;
|
|
452
|
+
display?:
|
|
453
|
+
| "auth"
|
|
454
|
+
| "anon"
|
|
455
|
+
| "always"
|
|
456
|
+
| "hide"
|
|
457
|
+
| ((params: { context: ZudokuContext; auth: UseAuthReturn }) => boolean);
|
|
458
|
+
};
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
</details>
|
|
462
|
+
|
|
266
463
|
## Display Control
|
|
267
464
|
|
|
268
465
|
All navigation items support a `display` property that controls when the item should be visible:
|
|
@@ -363,6 +560,94 @@ sidebar_label: Short Title
|
|
|
363
560
|
In this example, the document's title remains "My Long Title," but the sidebar displays "Short
|
|
364
561
|
Title."
|
|
365
562
|
|
|
563
|
+
For the complete list of supported frontmatter properties, see
|
|
564
|
+
[Frontmatter](/docs/markdown/frontmatter).
|
|
565
|
+
|
|
566
|
+
## Common Patterns
|
|
567
|
+
|
|
568
|
+
### Serving a document at the root URL
|
|
569
|
+
|
|
570
|
+
To make a markdown document accessible at `/`, use the `path` property to override the default
|
|
571
|
+
file-based route:
|
|
572
|
+
|
|
573
|
+
```tsx title="Standalone root doc"
|
|
574
|
+
navigation: [
|
|
575
|
+
{
|
|
576
|
+
type: "doc",
|
|
577
|
+
file: "home.md",
|
|
578
|
+
path: "/",
|
|
579
|
+
label: "Home",
|
|
580
|
+
},
|
|
581
|
+
],
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
```tsx title="Category with root landing page"
|
|
585
|
+
navigation: [
|
|
586
|
+
{
|
|
587
|
+
type: "category",
|
|
588
|
+
label: "Documentation",
|
|
589
|
+
link: {
|
|
590
|
+
type: "doc",
|
|
591
|
+
file: "home.md",
|
|
592
|
+
path: "/",
|
|
593
|
+
},
|
|
594
|
+
items: [
|
|
595
|
+
"guides/getting-started",
|
|
596
|
+
"guides/installation",
|
|
597
|
+
],
|
|
598
|
+
},
|
|
599
|
+
],
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
### Landing page with hidden tab
|
|
603
|
+
|
|
604
|
+
Use a `custom-page` with `display: "hide"` and `layout: "none"` to create a full-page landing
|
|
605
|
+
experience that doesn't appear in the navigation tabs:
|
|
606
|
+
|
|
607
|
+
```tsx
|
|
608
|
+
navigation: [
|
|
609
|
+
{
|
|
610
|
+
type: "custom-page",
|
|
611
|
+
path: "/",
|
|
612
|
+
display: "hide",
|
|
613
|
+
layout: "none",
|
|
614
|
+
element: <LandingPage />,
|
|
615
|
+
},
|
|
616
|
+
{
|
|
617
|
+
type: "category",
|
|
618
|
+
label: "Documentation",
|
|
619
|
+
items: ["docs/quickstart", "docs/installation"],
|
|
620
|
+
},
|
|
621
|
+
],
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
### Organized sidebar with sections and separators
|
|
625
|
+
|
|
626
|
+
Combine `section`, `separator`, and `filter` items to create a well-structured sidebar:
|
|
627
|
+
|
|
628
|
+
```tsx
|
|
629
|
+
navigation: [
|
|
630
|
+
{
|
|
631
|
+
type: "category",
|
|
632
|
+
label: "Documentation",
|
|
633
|
+
items: [
|
|
634
|
+
{ type: "filter", placeholder: "Filter documentation" },
|
|
635
|
+
{ type: "section", label: "Getting Started" },
|
|
636
|
+
"guides/quickstart",
|
|
637
|
+
"guides/installation",
|
|
638
|
+
{ type: "separator" },
|
|
639
|
+
{ type: "section", label: "Advanced" },
|
|
640
|
+
{
|
|
641
|
+
type: "category",
|
|
642
|
+
label: "Plugins",
|
|
643
|
+
icon: "blocks",
|
|
644
|
+
items: ["plugins/overview", "plugins/custom"],
|
|
645
|
+
},
|
|
646
|
+
],
|
|
647
|
+
},
|
|
648
|
+
],
|
|
649
|
+
```
|
|
650
|
+
|
|
366
651
|
## Navigation Rules
|
|
367
652
|
|
|
368
653
|
Plugins generate sidebar navigation automatically (e.g. from OpenAPI tags). Navigation rules let you
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Documenting MCP Servers
|
|
3
|
+
sidebar_icon: bot
|
|
4
|
+
zuplo: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Zudoku can render a dedicated [MCP](https://modelcontextprotocol.io/) endpoint UI for any OpenAPI
|
|
8
|
+
operation that has the `x-mcp-server` extension. When detected, the operation page replaces the
|
|
9
|
+
standard request/response view with an MCP card showing the endpoint URL, a copy button, and tabbed
|
|
10
|
+
installation instructions for Claude, ChatGPT, Cursor, VS Code, and a generic config.
|
|
11
|
+
|
|
12
|
+
## Adding the extension
|
|
13
|
+
|
|
14
|
+
Add the `x-mcp-server` extension to an operation in your OpenAPI spec. While MCP servers typically
|
|
15
|
+
use `POST`, the extension works on any HTTP method:
|
|
16
|
+
|
|
17
|
+
```json title="openapi.json (paths section)"
|
|
18
|
+
{
|
|
19
|
+
"paths": {
|
|
20
|
+
"/mcp": {
|
|
21
|
+
"post": {
|
|
22
|
+
"summary": "My MCP Server",
|
|
23
|
+
"description": "MCP endpoint for querying documentation.",
|
|
24
|
+
"operationId": "mcpEndpoint",
|
|
25
|
+
"x-mcp-server": {
|
|
26
|
+
"name": "my-mcp-server",
|
|
27
|
+
"version": "1.0.0",
|
|
28
|
+
"tools": [
|
|
29
|
+
{
|
|
30
|
+
"name": "search_docs",
|
|
31
|
+
"description": "Search the documentation"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"name": "get_page",
|
|
35
|
+
"description": "Retrieve a specific documentation page"
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
"responses": {
|
|
40
|
+
"200": {
|
|
41
|
+
"description": "MCP response"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The UI will display beneath the operation heading, showing the full MCP URL derived from the server
|
|
51
|
+
URL and the operation path.
|
|
52
|
+
|
|
53
|
+
You can also use the shorthand `"x-mcp-server": true` to enable the MCP UI without specifying any
|
|
54
|
+
metadata. In this case, the operation's `summary` is used as the server name.
|
|
55
|
+
|
|
56
|
+
## Extension properties
|
|
57
|
+
|
|
58
|
+
| Property | Type | Required | Description |
|
|
59
|
+
| --------- | -------- | -------- | ---------------------------------------------------------------------------------------------------------------------------- |
|
|
60
|
+
| `name` | `string` | No | Display name used in the generated client configuration snippets. Falls back to the operation `summary`, then `"mcp-server"` |
|
|
61
|
+
| `version` | `string` | No | Version metadata (included for completeness; not currently rendered in UI) |
|
|
62
|
+
| `tools` | `array` | No | Tools metadata (used by Zuplo enrichment; not currently rendered in UI) |
|
|
63
|
+
|
|
64
|
+
Each tool in the `tools` array has:
|
|
65
|
+
|
|
66
|
+
| Property | Type | Required | Description |
|
|
67
|
+
| ------------- | -------- | -------- | ------------------------------- |
|
|
68
|
+
| `name` | `string` | Yes | Tool name |
|
|
69
|
+
| `description` | `string` | No | Human-readable tool description |
|
|
70
|
+
|
|
71
|
+
## MCP URL resolution
|
|
72
|
+
|
|
73
|
+
The displayed MCP URL is constructed from the **server URL** of the API and the **path** of the
|
|
74
|
+
operation. The server URL comes from the OpenAPI `servers` array (or the operation-level `servers`
|
|
75
|
+
override if present).
|
|
76
|
+
|
|
77
|
+
For example, with this configuration:
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"servers": [{ "url": "https://api.example.com" }],
|
|
82
|
+
"paths": {
|
|
83
|
+
"/mcp/docs": {
|
|
84
|
+
"post": {
|
|
85
|
+
"x-mcp-server": { "name": "docs-mcp" },
|
|
86
|
+
"responses": { "200": { "description": "OK" } }
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The displayed MCP URL will be `https://api.example.com/mcp/docs`.
|
|
94
|
+
|
|
95
|
+
## Complete example
|
|
96
|
+
|
|
97
|
+
This is a minimal but complete OpenAPI spec that produces an MCP endpoint page:
|
|
98
|
+
|
|
99
|
+
```json title="mcp-api.json"
|
|
100
|
+
{
|
|
101
|
+
"openapi": "3.0.3",
|
|
102
|
+
"info": {
|
|
103
|
+
"title": "Documentation MCP Server",
|
|
104
|
+
"version": "1.0.0"
|
|
105
|
+
},
|
|
106
|
+
"servers": [
|
|
107
|
+
{
|
|
108
|
+
"url": "https://api.example.com",
|
|
109
|
+
"description": "Production"
|
|
110
|
+
}
|
|
111
|
+
],
|
|
112
|
+
"paths": {
|
|
113
|
+
"/mcp": {
|
|
114
|
+
"post": {
|
|
115
|
+
"tags": ["MCP"],
|
|
116
|
+
"summary": "Documentation MCP Server",
|
|
117
|
+
"description": "MCP endpoint powered by Inkeep for searching and querying documentation.",
|
|
118
|
+
"operationId": "mcpEndpoint",
|
|
119
|
+
"x-mcp-server": {
|
|
120
|
+
"name": "example-docs",
|
|
121
|
+
"version": "1.0.0",
|
|
122
|
+
"tools": [
|
|
123
|
+
{
|
|
124
|
+
"name": "search_docs",
|
|
125
|
+
"description": "Search the documentation"
|
|
126
|
+
}
|
|
127
|
+
]
|
|
128
|
+
},
|
|
129
|
+
"responses": {
|
|
130
|
+
"200": {
|
|
131
|
+
"description": "MCP response"
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Then reference this spec in your Zudoku config (see
|
|
141
|
+
[API Reference](/docs/configuration/api-reference) for full `apis` configuration):
|
|
142
|
+
|
|
143
|
+
```tsx title="zudoku.config.tsx"
|
|
144
|
+
import type { ZudokuConfig } from "zudoku";
|
|
145
|
+
|
|
146
|
+
const config: ZudokuConfig = {
|
|
147
|
+
apis: [
|
|
148
|
+
{
|
|
149
|
+
type: "file",
|
|
150
|
+
input: "./mcp-api.json",
|
|
151
|
+
path: "mcp",
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
navigation: [
|
|
155
|
+
{
|
|
156
|
+
type: "link",
|
|
157
|
+
label: "MCP Server",
|
|
158
|
+
to: "/mcp",
|
|
159
|
+
icon: "bot",
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
export default config;
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
You can see a live example of this in the
|
|
168
|
+
[Cosmo Cargo demo](https://www.cosmocargo.dev/catalog/api-ai-cargo/ai-operations#universal-mcp-endpoint).
|
|
169
|
+
|
|
170
|
+
## Generated UI
|
|
171
|
+
|
|
172
|
+
When Zudoku detects the `x-mcp-server` extension on an operation, the page shows:
|
|
173
|
+
|
|
174
|
+
- **MCP Endpoint card** with the full URL and a copy button
|
|
175
|
+
- **AI Tool Configuration** tabs with setup instructions for:
|
|
176
|
+
- **Claude** — `claude_desktop_config.json` using native HTTP transport
|
|
177
|
+
- **ChatGPT** — connector setup via Settings
|
|
178
|
+
- **Cursor** — `mcp.json` configuration (global or project-level)
|
|
179
|
+
- **VS Code** — `.vscode/mcp.json` for GitHub Copilot
|
|
180
|
+
- **Generic** — standard `mcp.json` format compatible with most MCP clients
|
|
181
|
+
|
|
182
|
+
The standard method badge, request body, parameters, and sidecar panels are hidden for MCP endpoints
|
|
183
|
+
since they use a different interaction model.
|
|
184
|
+
|
|
185
|
+
## Using with Zuplo
|
|
186
|
+
|
|
187
|
+
If you are using [Zuplo](https://zuplo.com) to host your API, the `x-mcp-server` extension is
|
|
188
|
+
automatically added to POST operations that use the `mcpServerHandler`. No manual schema changes are
|
|
189
|
+
needed. See the [Zuplo MCP documentation](https://zuplo.com/docs/handlers/mcp-handler) for details.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zudoku",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.73.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": [
|
|
6
6
|
"**/*.css",
|
|
@@ -130,7 +130,6 @@
|
|
|
130
130
|
"types": "./dist/declarations/lib/hooks/index.d.ts",
|
|
131
131
|
"default": "./src/lib/hooks/index.ts"
|
|
132
132
|
},
|
|
133
|
-
"./main.css": "./src/app/main.css",
|
|
134
133
|
"./processors/*": {
|
|
135
134
|
"types": "./dist/declarations/lib/plugins/openapi/processors/*.d.ts",
|
|
136
135
|
"default": "./src/lib/plugins/openapi/processors/*.ts"
|
|
@@ -259,10 +258,10 @@
|
|
|
259
258
|
"vaul": "1.1.2",
|
|
260
259
|
"vfile": "6.0.3",
|
|
261
260
|
"vite": "7.3.1",
|
|
262
|
-
"yaml": "2.8.
|
|
261
|
+
"yaml": "2.8.3",
|
|
263
262
|
"yargs": "18.0.0",
|
|
264
263
|
"zod": "4.3.6",
|
|
265
|
-
"zustand": "5.0.
|
|
264
|
+
"zustand": "5.0.12"
|
|
266
265
|
},
|
|
267
266
|
"devDependencies": {
|
|
268
267
|
"@clerk/clerk-js": "^5.125.3",
|
|
@@ -13,19 +13,17 @@ const errorDetailsMap: Record<string, { message: string }> = {
|
|
|
13
13
|
"The authentication request was invalid. Please try signing in again.",
|
|
14
14
|
},
|
|
15
15
|
unauthorized_client: {
|
|
16
|
-
message:
|
|
17
|
-
"This application is not authorized to access your account. Please contact support.",
|
|
16
|
+
message: "This application is not authorized to access your account.",
|
|
18
17
|
},
|
|
19
18
|
access_denied: {
|
|
20
19
|
message:
|
|
21
20
|
"You denied access to this application. To continue, please sign in and grant access.",
|
|
22
21
|
},
|
|
23
22
|
unsupported_response_type: {
|
|
24
|
-
message:
|
|
25
|
-
"The authentication method is not supported. Please contact support.",
|
|
23
|
+
message: "The authentication method is not supported.",
|
|
26
24
|
},
|
|
27
25
|
invalid_scope: {
|
|
28
|
-
message: "The requested permissions are invalid.
|
|
26
|
+
message: "The requested permissions are invalid.",
|
|
29
27
|
},
|
|
30
28
|
server_error: {
|
|
31
29
|
message:
|
|
@@ -37,15 +35,14 @@ const errorDetailsMap: Record<string, { message: string }> = {
|
|
|
37
35
|
},
|
|
38
36
|
// Token errors
|
|
39
37
|
invalid_client: {
|
|
40
|
-
message: "Invalid application credentials.
|
|
38
|
+
message: "Invalid application credentials.",
|
|
41
39
|
},
|
|
42
40
|
invalid_grant: {
|
|
43
41
|
message:
|
|
44
42
|
"The authentication code has expired or is invalid. Please sign in again.",
|
|
45
43
|
},
|
|
46
44
|
unsupported_grant_type: {
|
|
47
|
-
message:
|
|
48
|
-
"The authentication method is not supported. Please contact support.",
|
|
45
|
+
message: "The authentication method is not supported.",
|
|
49
46
|
},
|
|
50
47
|
// Custom errors
|
|
51
48
|
invalid_state: {
|
|
@@ -64,8 +61,7 @@ const errorDetailsMap: Record<string, { message: string }> = {
|
|
|
64
61
|
message: "Your authentication session has expired. Please sign in again.",
|
|
65
62
|
},
|
|
66
63
|
configuration_error: {
|
|
67
|
-
message:
|
|
68
|
-
"There is an issue with the authentication configuration. Please contact support.",
|
|
64
|
+
message: "There is an issue with the authentication configuration.",
|
|
69
65
|
},
|
|
70
66
|
unknown_error: {
|
|
71
67
|
message:
|
|
@@ -13,7 +13,7 @@ import type {
|
|
|
13
13
|
import { CoreAuthenticationPlugin } from "../AuthenticationPlugin.js";
|
|
14
14
|
import { CallbackHandler } from "../components/CallbackHandler.js";
|
|
15
15
|
import { OAuthErrorPage } from "../components/OAuthErrorPage.js";
|
|
16
|
-
import { AuthorizationError } from "../errors.js";
|
|
16
|
+
import { AuthorizationError, OAuthAuthorizationError } from "../errors.js";
|
|
17
17
|
import { type UserProfile, useAuthState } from "../state.js";
|
|
18
18
|
|
|
19
19
|
const CODE_VERIFIER_KEY = "code-verifier";
|
|
@@ -106,6 +106,19 @@ export class OpenIDAuthenticationProvider
|
|
|
106
106
|
throw new AuthorizationError("No expires_in in response");
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
const accessToken = response.access_token;
|
|
110
|
+
if (accessToken.split(".").length !== 3) {
|
|
111
|
+
throw new OAuthAuthorizationError(
|
|
112
|
+
"The access token received is not a valid JWT.",
|
|
113
|
+
{
|
|
114
|
+
error: "configuration_error",
|
|
115
|
+
error_description:
|
|
116
|
+
"The authentication provider is issuing opaque tokens instead of JWTs. " +
|
|
117
|
+
"Ensure you have configured the correct `audience` in your authentication settings.",
|
|
118
|
+
},
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
109
122
|
const claims = response.id_token
|
|
110
123
|
? oauth.getValidatedIdTokenClaims(response)
|
|
111
124
|
: undefined;
|
|
@@ -17,7 +17,7 @@ export const DeveloperHint = ({
|
|
|
17
17
|
if (process.env.NODE_ENV !== "development") return null;
|
|
18
18
|
|
|
19
19
|
return (
|
|
20
|
-
<Alert variant="info" className={className}>
|
|
20
|
+
<Alert variant="info" className={className} fit="loose">
|
|
21
21
|
<InfoIcon />
|
|
22
22
|
<AlertTitle>Developer hint</AlertTitle>
|
|
23
23
|
<AlertDescription>
|
|
@@ -12,9 +12,9 @@ import { Slot } from "./Slot.js";
|
|
|
12
12
|
import { Spinner } from "./Spinner.js";
|
|
13
13
|
|
|
14
14
|
const LoadingFallback = () => (
|
|
15
|
-
<
|
|
15
|
+
<div className="col-span-full row-span-full grid place-items-center">
|
|
16
16
|
<Spinner />
|
|
17
|
-
</
|
|
17
|
+
</div>
|
|
18
18
|
);
|
|
19
19
|
|
|
20
20
|
export const Layout = ({ children }: { children?: ReactNode }) => {
|
|
@@ -11,15 +11,17 @@ import { cn } from "../../util/cn.js";
|
|
|
11
11
|
|
|
12
12
|
export const MCPEndpoint = ({
|
|
13
13
|
serverUrl,
|
|
14
|
+
operationPath,
|
|
14
15
|
summary,
|
|
15
16
|
data,
|
|
16
17
|
}: {
|
|
17
18
|
serverUrl?: string;
|
|
19
|
+
operationPath?: string;
|
|
18
20
|
data?: boolean | Record<string, unknown>;
|
|
19
21
|
summary?: string;
|
|
20
22
|
}) => {
|
|
21
23
|
const [isCopied, setIsCopied] = useState(false);
|
|
22
|
-
const mcpUrl = `${(serverUrl ?? "").replace(/\/+$/, "")}/mcp`;
|
|
24
|
+
const mcpUrl = `${(serverUrl ?? "").replace(/\/+$/, "")}${operationPath ?? "/mcp"}`;
|
|
23
25
|
|
|
24
26
|
const name =
|
|
25
27
|
typeof data === "boolean"
|
|
@@ -29,11 +31,8 @@ export const MCPEndpoint = ({
|
|
|
29
31
|
const claudeConfig = `{
|
|
30
32
|
"mcpServers": {
|
|
31
33
|
"${name}": {
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"mcp-remote",
|
|
35
|
-
"${mcpUrl}"
|
|
36
|
-
]
|
|
34
|
+
"type": "http",
|
|
35
|
+
"url": "${mcpUrl}"
|
|
37
36
|
}
|
|
38
37
|
}
|
|
39
38
|
}`;
|
|
@@ -48,6 +47,14 @@ export const MCPEndpoint = ({
|
|
|
48
47
|
|
|
49
48
|
const chatgptConfig = mcpUrl;
|
|
50
49
|
|
|
50
|
+
const genericConfig = `{
|
|
51
|
+
"mcpServers": {
|
|
52
|
+
"${name}": {
|
|
53
|
+
"url": "${mcpUrl}"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}`;
|
|
57
|
+
|
|
51
58
|
const vscodeConfig = `{
|
|
52
59
|
"servers": {
|
|
53
60
|
"${name}": {
|
|
@@ -117,11 +124,12 @@ export const MCPEndpoint = ({
|
|
|
117
124
|
<hr className="my-4" />
|
|
118
125
|
|
|
119
126
|
<Tabs defaultValue="claude" className="w-full">
|
|
120
|
-
<TabsList className="grid w-full grid-cols-
|
|
127
|
+
<TabsList className="grid w-full grid-cols-5">
|
|
121
128
|
<TabsTrigger value="claude">Claude</TabsTrigger>
|
|
122
129
|
<TabsTrigger value="chatgpt">ChatGPT</TabsTrigger>
|
|
123
130
|
<TabsTrigger value="cursor">Cursor</TabsTrigger>
|
|
124
131
|
<TabsTrigger value="vscode">VS Code</TabsTrigger>
|
|
132
|
+
<TabsTrigger value="generic">Generic</TabsTrigger>
|
|
125
133
|
</TabsList>
|
|
126
134
|
|
|
127
135
|
<Typography className="text-sm max-w-full">
|
|
@@ -264,6 +272,34 @@ export const MCPEndpoint = ({
|
|
|
264
272
|
<ExternalLinkIcon className="h-3 w-3" />
|
|
265
273
|
</a>
|
|
266
274
|
</TabsContent>
|
|
275
|
+
|
|
276
|
+
<TabsContent value="generic" className="space-y-3">
|
|
277
|
+
<p>
|
|
278
|
+
Generic <InlineCode>.mcp.json</InlineCode> configuration
|
|
279
|
+
format that works with most MCP-compatible AI tools.
|
|
280
|
+
</p>
|
|
281
|
+
<SyntaxHighlight
|
|
282
|
+
showLanguageIndicator
|
|
283
|
+
title=".mcp.json"
|
|
284
|
+
language="json"
|
|
285
|
+
code={genericConfig}
|
|
286
|
+
className="mt-2"
|
|
287
|
+
/>
|
|
288
|
+
<p className="text-sm text-muted-foreground">
|
|
289
|
+
Place this file in your project root or the appropriate
|
|
290
|
+
configuration directory for your AI tool. The exact location
|
|
291
|
+
depends on your specific tool.
|
|
292
|
+
</p>
|
|
293
|
+
<a
|
|
294
|
+
href="https://modelcontextprotocol.io/"
|
|
295
|
+
target="_blank"
|
|
296
|
+
rel="noopener noreferrer"
|
|
297
|
+
className="inline-flex items-center gap-1 text-sm text-primary hover:underline"
|
|
298
|
+
>
|
|
299
|
+
Learn more about MCP
|
|
300
|
+
<ExternalLinkIcon className="h-3 w-3" />
|
|
301
|
+
</a>
|
|
302
|
+
</TabsContent>
|
|
267
303
|
</Typography>
|
|
268
304
|
</Tabs>
|
|
269
305
|
</div>
|
|
@@ -256,12 +256,13 @@ export const Playground = ({
|
|
|
256
256
|
break;
|
|
257
257
|
}
|
|
258
258
|
|
|
259
|
+
const upperMethod = method.toUpperCase();
|
|
259
260
|
const request = new Request(
|
|
260
261
|
createUrl(server ?? selectedServer, url, data),
|
|
261
262
|
{
|
|
262
|
-
method,
|
|
263
|
+
method: upperMethod,
|
|
263
264
|
headers,
|
|
264
|
-
body: ["GET", "HEAD"].includes(
|
|
265
|
+
body: ["GET", "HEAD"].includes(upperMethod) ? null : body,
|
|
265
266
|
},
|
|
266
267
|
);
|
|
267
268
|
|
|
@@ -153,7 +153,7 @@ const ObjectSchemaView = ({
|
|
|
153
153
|
return properties ? { group, properties } : [];
|
|
154
154
|
});
|
|
155
155
|
|
|
156
|
-
const deprecatedProperties = groupedProperties
|
|
156
|
+
const deprecatedProperties = groupedProperties.deprecated;
|
|
157
157
|
|
|
158
158
|
const additionalObjectProperties = typeof schema.additionalProperties ===
|
|
159
159
|
"object" && <SchemaView schema={schema.additionalProperties} embedded />;
|
|
@@ -41,6 +41,26 @@ const normalizeProblemJson = (data: unknown): ProblemJson | undefined => {
|
|
|
41
41
|
} as ProblemJson;
|
|
42
42
|
};
|
|
43
43
|
|
|
44
|
+
/** Some gateways return RFC 7807-shaped bodies with `application/json`. */
|
|
45
|
+
const normalizeProblemJsonIfProblemLike = (
|
|
46
|
+
data: unknown,
|
|
47
|
+
): ProblemJson | undefined => {
|
|
48
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const record = data as Record<string, unknown>;
|
|
53
|
+
const hasDetail = typeof record.detail === "string";
|
|
54
|
+
const hasTitle = typeof record.title === "string";
|
|
55
|
+
const hasStatus = typeof record.status === "number";
|
|
56
|
+
|
|
57
|
+
if (!hasDetail && !(hasTitle && hasStatus)) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return normalizeProblemJson(data);
|
|
62
|
+
};
|
|
63
|
+
|
|
44
64
|
export const getProblemJson = async (
|
|
45
65
|
response: Response,
|
|
46
66
|
): Promise<ProblemJson | undefined> => {
|
|
@@ -54,10 +74,25 @@ export const getProblemJson = async (
|
|
|
54
74
|
};
|
|
55
75
|
|
|
56
76
|
export const throwIfProblemJson = async (response: Response) => {
|
|
57
|
-
if (
|
|
77
|
+
if (response.ok) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
82
|
+
|
|
83
|
+
if (isProblemJsonContentType(response)) {
|
|
58
84
|
const problem = await getProblemJson(response);
|
|
59
85
|
if (problem) {
|
|
60
86
|
throw new Error(problem.detail ?? problem.title ?? "Unknown error");
|
|
61
87
|
}
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (contentType.includes("application/json")) {
|
|
92
|
+
const data = await parseJsonSafe(response.clone());
|
|
93
|
+
const problem = normalizeProblemJsonIfProblemLike(data);
|
|
94
|
+
if (problem) {
|
|
95
|
+
throw new Error(problem.detail ?? problem.title ?? "Unknown error");
|
|
96
|
+
}
|
|
62
97
|
}
|
|
63
98
|
};
|
|
@@ -49,6 +49,12 @@ const processMarkdownFile = async (
|
|
|
49
49
|
*
|
|
50
50
|
* It also writes metadata to markdown-info.json used by the llms.txt generator.
|
|
51
51
|
*/
|
|
52
|
+
export const getMarkdownOutputPath = (distDir: string, routePath: string) => {
|
|
53
|
+
const segments =
|
|
54
|
+
routePath === "/" ? ["index"] : routePath.split("/").filter(Boolean);
|
|
55
|
+
return `${path.join(distDir, ...segments)}.md`;
|
|
56
|
+
};
|
|
57
|
+
|
|
52
58
|
const viteMarkdownExportPlugin = (): Plugin => {
|
|
53
59
|
let markdownFiles: Record<string, string> = {};
|
|
54
60
|
let markdownFileInfos: MarkdownFileInfo[] = [];
|
|
@@ -180,12 +186,7 @@ const viteMarkdownExportPlugin = (): Plugin => {
|
|
|
180
186
|
content: finalMarkdown,
|
|
181
187
|
});
|
|
182
188
|
|
|
183
|
-
const
|
|
184
|
-
routePath === "/"
|
|
185
|
-
? ["index"]
|
|
186
|
-
: routePath.split("/").filter(Boolean);
|
|
187
|
-
|
|
188
|
-
const outputPath = `${path.join(distDir, ...segments)}.md`;
|
|
189
|
+
const outputPath = getMarkdownOutputPath(distDir, routePath);
|
|
189
190
|
|
|
190
191
|
await mkdir(path.dirname(outputPath), { recursive: true });
|
|
191
192
|
|
|
@@ -13,7 +13,10 @@ import type { ZudokuConfig } from "../../config/validators/ZudokuConfig.js";
|
|
|
13
13
|
import { runPluginTransformConfig } from "../../lib/core/transform-config.js";
|
|
14
14
|
import invariant from "../../lib/util/invariant.js";
|
|
15
15
|
import { joinUrl } from "../../lib/util/joinUrl.js";
|
|
16
|
-
import
|
|
16
|
+
import {
|
|
17
|
+
getMarkdownOutputPath,
|
|
18
|
+
type MarkdownFileInfo,
|
|
19
|
+
} from "../plugin-markdown-export.js";
|
|
17
20
|
import { isTTY, throttle, writeLine } from "../reporter.js";
|
|
18
21
|
import { generateSitemap } from "../sitemap.js";
|
|
19
22
|
import { routesToPaths, routesToRewrites } from "./utils.js";
|
|
@@ -224,7 +227,13 @@ export const prerender = async ({
|
|
|
224
227
|
|
|
225
228
|
if (!docsConfig.publishMarkdown) {
|
|
226
229
|
await Promise.all(
|
|
227
|
-
markdownFileInfos.map((info) =>
|
|
230
|
+
markdownFileInfos.map((info) => {
|
|
231
|
+
const outputPath = getMarkdownOutputPath(distDir, info.routePath);
|
|
232
|
+
if (!path.resolve(outputPath).startsWith(path.resolve(distDir))) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
return rm(outputPath).catch(() => {});
|
|
236
|
+
}),
|
|
228
237
|
);
|
|
229
238
|
}
|
|
230
239
|
}
|