zudoku 0.74.3 → 0.75.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 CHANGED
@@ -3813,7 +3813,7 @@ import {
3813
3813
  // package.json
3814
3814
  var package_default = {
3815
3815
  name: "zudoku",
3816
- version: "0.74.2",
3816
+ version: "0.74.3",
3817
3817
  type: "module",
3818
3818
  sideEffects: [
3819
3819
  "**/*.css",
@@ -3951,7 +3951,7 @@ var package_default = {
3951
3951
  "fast-equals": "6.0.0",
3952
3952
  glob: "13.0.6",
3953
3953
  "glob-parent": "6.0.2",
3954
- graphql: "16.13.1",
3954
+ graphql: "16.13.2",
3955
3955
  "graphql-type-json": "0.3.2",
3956
3956
  "graphql-yoga": "5.18.0",
3957
3957
  "gray-matter": "4.0.3",
@@ -7775,6 +7775,7 @@ async function writeOutput(dir, {
7775
7775
  // src/vite/prerender/prerender.ts
7776
7776
  init_logger();
7777
7777
  init_file_exists();
7778
+ import { readFileSync as readFileSync2 } from "node:fs";
7778
7779
  import { readFile as readFile2, rm } from "node:fs/promises";
7779
7780
  import os from "node:os";
7780
7781
  import path21 from "node:path";
@@ -7945,7 +7946,9 @@ var prerender = async ({
7945
7946
  paths.push(joinUrl(r.from));
7946
7947
  }
7947
7948
  }
7948
- const maxThreads = buildConfig?.prerender?.workers ?? Math.floor(os.cpus().length * 0.8);
7949
+ const { maxThreads, maxOldGenerationSizeMb } = getWorkerScaling(
7950
+ buildConfig?.prerender?.workers
7951
+ );
7949
7952
  const start = performance.now();
7950
7953
  const LOG_INTERVAL_MS = 3e4;
7951
7954
  let lastLogTime = start;
@@ -7974,7 +7977,11 @@ var prerender = async ({
7974
7977
  const pool = new Piscina({
7975
7978
  filename: new URL("./worker.js", import.meta.url).href,
7976
7979
  idleTimeout: 5e3,
7980
+ minThreads: 1,
7977
7981
  maxThreads,
7982
+ resourceLimits: {
7983
+ maxOldGenerationSizeMb
7984
+ },
7978
7985
  workerData: {
7979
7986
  template: html,
7980
7987
  distDir,
@@ -7986,12 +7993,6 @@ var prerender = async ({
7986
7993
  const workerResults = await Promise.all(
7987
7994
  paths.map(async (urlPath) => {
7988
7995
  const result = await pool.run({ urlPath });
7989
- if (result.statusCode < 400) {
7990
- await pagefindIndex?.addHTMLFile({
7991
- url: urlPath,
7992
- content: result.html
7993
- });
7994
- }
7995
7996
  completedCount++;
7996
7997
  if (isTTY()) {
7997
7998
  writeProgress(completedCount, paths.length, urlPath);
@@ -8009,9 +8010,6 @@ var prerender = async ({
8009
8010
  return result;
8010
8011
  })
8011
8012
  );
8012
- const pagefindWriteResult = await pagefindIndex?.writeFiles({
8013
- outputPath: path21.join(distDir, "pagefind")
8014
- });
8015
8013
  const seconds = ((performance.now() - start) / 1e3).toFixed(1);
8016
8014
  const message = `\u2713 finished prerendering ${paths.length} routes in ${seconds} seconds using ${maxThreads} workers`;
8017
8015
  if (isTTY()) {
@@ -8020,10 +8018,38 @@ var prerender = async ({
8020
8018
  } else {
8021
8019
  logger.info(colors7.blue(message));
8022
8020
  }
8023
- if (pagefindWriteResult?.outputPath) {
8024
- logger.info(
8025
- colors7.blue(`\u2713 pagefind index built: ${pagefindWriteResult.outputPath}`)
8021
+ if (pagefindIndex) {
8022
+ const pagesToIndex = workerResults.flatMap(
8023
+ ({ statusCode, html: html2 }, i) => statusCode < 400 ? { url: paths[i], html: html2 } : []
8026
8024
  );
8025
+ const BATCH_SIZE = 40;
8026
+ const pagefindStart = performance.now();
8027
+ for (let offset = 0; offset < pagesToIndex.length; offset += BATCH_SIZE) {
8028
+ const batch = pagesToIndex.slice(offset, offset + BATCH_SIZE);
8029
+ await Promise.all(
8030
+ batch.map(
8031
+ ({ url, html: html2 }) => pagefindIndex.addHTMLFile({ url, content: html2 })
8032
+ )
8033
+ );
8034
+ if (isTTY()) {
8035
+ const done = offset + batch.length;
8036
+ writeLine(
8037
+ `pagefind indexing (${done}/${pagesToIndex.length}) ${colors7.dim(batch.at(-1)?.url ?? "")}`
8038
+ );
8039
+ }
8040
+ }
8041
+ if (isTTY()) writeLine("");
8042
+ const { outputPath } = await pagefindIndex.writeFiles({
8043
+ outputPath: path21.join(distDir, "pagefind")
8044
+ });
8045
+ if (outputPath) {
8046
+ const duration = (performance.now() - pagefindStart) / 1e3;
8047
+ logger.info(
8048
+ colors7.blue(
8049
+ `\u2713 pagefind index built in ${duration.toFixed(1)} seconds: ${outputPath}`
8050
+ )
8051
+ );
8052
+ }
8027
8053
  }
8028
8054
  const redirectUrls = getRedirectUrls(workerResults, config2.basePath);
8029
8055
  await generateSitemap({
@@ -8074,6 +8100,52 @@ var prerender = async ({
8074
8100
  }
8075
8101
  return { workerResults, rewrites };
8076
8102
  };
8103
+ var getWorkerScaling = (workersOverride) => {
8104
+ const PER_WORKER_HEAP_LIMIT_MB = 4096;
8105
+ const MAX_WORKERS = 8;
8106
+ const osTotalMb = Math.floor(os.totalmem() / (1024 * 1024));
8107
+ const cgroupMemMb = getContainerMemoryLimitMb();
8108
+ const totalMemMb = cgroupMemMb !== void 0 ? Math.min(cgroupMemMb, osTotalMb) : osTotalMb;
8109
+ const reservedMb = Math.max(2048, Math.floor(totalMemMb * 0.25));
8110
+ const availableForWorkersMb = totalMemMb - reservedMb;
8111
+ const memBasedWorkers = Math.max(
8112
+ 1,
8113
+ Math.floor(availableForWorkersMb / PER_WORKER_HEAP_LIMIT_MB)
8114
+ );
8115
+ const cpuBasedWorkers = Math.max(1, Math.floor(os.cpus().length * 0.5));
8116
+ const defaultWorkers = Math.min(
8117
+ memBasedWorkers,
8118
+ cpuBasedWorkers,
8119
+ MAX_WORKERS
8120
+ );
8121
+ const validOverride = workersOverride && workersOverride > 0 ? workersOverride : void 0;
8122
+ const maxThreads = validOverride ?? defaultWorkers;
8123
+ const maxOldGenerationSizeMb = Math.min(
8124
+ PER_WORKER_HEAP_LIMIT_MB,
8125
+ Math.max(512, Math.floor(availableForWorkersMb / maxThreads))
8126
+ );
8127
+ return { maxThreads, maxOldGenerationSizeMb };
8128
+ };
8129
+ var getContainerMemoryLimitMb = () => {
8130
+ const CGROUP_PATHS = [
8131
+ "/sys/fs/cgroup/memory.max",
8132
+ // cgroup v2
8133
+ "/sys/fs/cgroup/memory/memory.limit_in_bytes"
8134
+ // cgroup v1
8135
+ ];
8136
+ for (const filePath of CGROUP_PATHS) {
8137
+ try {
8138
+ const raw = readFileSync2(filePath, "utf8").trim();
8139
+ if (raw === "max") return void 0;
8140
+ const bytes = Number(raw);
8141
+ if (!Number.isFinite(bytes) || bytes > 2 ** 50) return void 0;
8142
+ return Math.floor(bytes / (1024 * 1024));
8143
+ } catch (err) {
8144
+ if (err.code !== "ENOENT") throw err;
8145
+ }
8146
+ }
8147
+ return void 0;
8148
+ };
8077
8149
 
8078
8150
  // src/vite/build.ts
8079
8151
  var DIST_DIR = "dist";
@@ -1,6 +1,7 @@
1
+ import { type McpServerData } from "./mcp-configs.js";
1
2
  export declare const MCPEndpoint: ({ serverUrl, operationPath, summary, data, }: {
2
3
  serverUrl?: string;
3
4
  operationPath?: string;
4
- data?: boolean | Record<string, unknown>;
5
+ data?: McpServerData;
5
6
  summary?: string;
6
7
  }) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,28 @@
1
+ export type McpServerData = boolean | Record<string, unknown>;
2
+ export interface AuthHeader {
3
+ headerName: string;
4
+ placeholder: string;
5
+ }
6
+ export type AuthType = "none" | "apiKey" | "oauth";
7
+ export declare const getAuthType: (data?: McpServerData) => AuthType;
8
+ export declare const getAuthHeader: (data?: McpServerData) => AuthHeader | undefined;
9
+ export interface McpSubApp {
10
+ id: string;
11
+ label: string;
12
+ supportedAuth: AuthType[];
13
+ }
14
+ export interface McpApp {
15
+ id: string;
16
+ label: string;
17
+ subApps: McpSubApp[];
18
+ }
19
+ export declare const MCP_APPS: McpApp[];
20
+ export declare const getVisibleApps: (authType: AuthType) => McpApp[];
21
+ export declare const getMcpServerName: (data?: McpServerData, summary?: string) => string;
22
+ export declare const getMcpUrl: (serverUrl?: string, operationPath?: string) => string;
23
+ export declare const getClaudeCodeCommand: (name: string, mcpUrl: string, auth?: AuthHeader) => string;
24
+ export declare const getCodexCliCommand: (name: string, mcpUrl: string, auth?: AuthHeader) => string;
25
+ export declare const getCursorConfig: (name: string, mcpUrl: string, auth?: AuthHeader) => string;
26
+ export declare const getVscodeConfig: (name: string, mcpUrl: string, auth?: AuthHeader) => string;
27
+ export declare const getCodexConfig: (name: string, mcpUrl: string, auth?: AuthHeader) => string;
28
+ export declare const getGenericConfig: (name: string, mcpUrl: string, auth?: AuthHeader) => string;
@@ -34,7 +34,10 @@ If you don't have an Auth0 account, you can sign up for a
34
34
  - Production: `https://your-site.com/oauth/callback`
35
35
  - Preview (wildcard): `https://*.your-domain.com/oauth/callback`
36
36
  - Local Development: `http://localhost:3000/oauth/callback`
37
- - **Allowed Logout URLs**: Same as callback URLs above
37
+ - **Allowed Logout URLs**:
38
+ - Production: `https://your-site.com/oauth/logout-callback`
39
+ - Preview (wildcard): `https://*.your-domain.com/oauth/logout-callback`
40
+ - Local Development: `http://localhost:3000/oauth/logout-callback`
38
41
 
39
42
  - **Allowed Web Origins**:
40
43
  - Production: `https://your-site.com`
@@ -112,8 +115,8 @@ To enable logout for your Auth0 application:
112
115
 
113
116
  1. Ensure your **Allowed Logout URLs** are configured in Auth0 (see
114
117
  [Configure Auth0 Application](#setup-steps) above)
115
- 2. The logout URL should match your callback URL pattern (e.g., `https://your-site.com/` for
116
- production)
118
+ 2. The logout URL must use the `/oauth/logout-callback` path (e.g.,
119
+ `https://your-site.com/oauth/logout-callback` for production)
117
120
 
118
121
  For older tenants, you may need to enable **RP-Initiated Logout** in your tenant settings. See the
119
122
  [Auth0 logout documentation](https://auth0.com/docs/authenticate/login/logout/log-users-out-of-auth0)
@@ -0,0 +1,110 @@
1
+ ---
2
+ title: OpenID Connect (OIDC)
3
+ sidebar_label: OpenID Connect
4
+ description:
5
+ Configure any OpenID Connect compliant identity provider (Okta, Keycloak, Authentik, etc.) as the
6
+ authentication provider for Zudoku.
7
+ ---
8
+
9
+ Zudoku supports any identity provider that implements the
10
+ [OpenID Connect](https://openid.net/specs/openid-connect-core-1_0.html) protocol via the generic
11
+ `openid` provider type. This includes Okta, Keycloak, Authentik, Ory, ZITADEL, AWS Cognito, Google
12
+ Identity, and most enterprise IdPs.
13
+
14
+ ## Configuration
15
+
16
+ Add the `authentication` property to your [Zudoku configuration](./overview.md):
17
+
18
+ ```typescript title="zudoku.config.ts"
19
+ {
20
+ // ...
21
+ authentication: {
22
+ type: "openid",
23
+ clientId: "<your-client-id>",
24
+ issuer: "<the-issuer-url>",
25
+ scopes: ["openid", "profile", "email"], // Optional
26
+ },
27
+ // ...
28
+ }
29
+ ```
30
+
31
+ | Option | Required | Description |
32
+ | ---------- | -------- | -------------------------------------------------------------------------------------------- |
33
+ | `clientId` | Yes | The OAuth client ID issued by your provider. |
34
+ | `issuer` | Yes | The issuer URL. Zudoku discovers endpoints from `<issuer>/.well-known/openid-configuration`. |
35
+ | `scopes` | No | Scopes to request. Defaults to `["openid", "profile", "email"]`. |
36
+
37
+ ## Provider Setup
38
+
39
+ Register Zudoku as a public SPA / single page application client in your identity provider and set:
40
+
41
+ - Callback / Redirect URI to `https://your-site.com/oauth/callback`
42
+ - For local development, add `http://localhost:3000/oauth/callback`
43
+ - If your provider supports wildcards, add `https://*.your-domain.com/oauth/callback` for preview
44
+ environments
45
+ - Add your site origin to the list of allowed CORS origins
46
+ - Enable the `Authorization Code` grant with PKCE and the `Refresh Token` grant
47
+
48
+ ### Okta
49
+
50
+ 1. In the Okta admin console go to **Applications** → **Applications** → **Create App Integration**.
51
+ 2. Select **OIDC - OpenID Connect** and **Single Page Application**.
52
+ 3. Set **Sign-in redirect URIs** to `https://your-site.com/oauth/callback` (add
53
+ `http://localhost:3000/oauth/callback` for local development).
54
+ 4. Under **Assignments**, assign the users or groups that should have access.
55
+ 5. After creating the app, copy the **Client ID**. Your issuer is your Okta domain, for example
56
+ `https://your-tenant.okta.com` or a custom authorization server like
57
+ `https://your-tenant.okta.com/oauth2/default`.
58
+ 6. Under **Security** → **API** → **Trusted Origins**, add your site origin for both CORS and
59
+ Redirect.
60
+
61
+ ```typescript title="zudoku.config.ts"
62
+ {
63
+ authentication: {
64
+ type: "openid",
65
+ clientId: "<your-okta-client-id>",
66
+ issuer: "https://your-tenant.okta.com/oauth2/default",
67
+ scopes: ["openid", "profile", "email"],
68
+ },
69
+ }
70
+ ```
71
+
72
+ ### Keycloak
73
+
74
+ Use the realm issuer URL:
75
+
76
+ ```typescript title="zudoku.config.ts"
77
+ {
78
+ authentication: {
79
+ type: "openid",
80
+ clientId: "zudoku",
81
+ issuer: "https://keycloak.example.com/realms/<your-realm>",
82
+ },
83
+ }
84
+ ```
85
+
86
+ In the realm, create a client with **Client type** `OpenID Connect`, **Access type** `public`, and
87
+ enable **Standard Flow** (Authorization Code).
88
+
89
+ ## Verifying the Issuer
90
+
91
+ You can confirm your issuer URL is correct by opening `<issuer>/.well-known/openid-configuration` in
92
+ a browser. It should return a JSON document listing `authorization_endpoint`, `token_endpoint`,
93
+ `userinfo_endpoint`, and `jwks_uri`.
94
+
95
+ ## User Profile
96
+
97
+ After sign-in Zudoku calls the provider's
98
+ [UserInfo endpoint](https://openid.net/specs/openid-connect-core-1_0.html#UserInfo) and reads
99
+ `name`, `email`, `picture`, and `email_verified` from the response. Map these claims in your
100
+ provider if they are not emitted by default.
101
+
102
+ ## Troubleshooting
103
+
104
+ - **Discovery fails**: verify `<issuer>/.well-known/openid-configuration` resolves and matches the
105
+ `issuer` value in the document.
106
+ - **CORS errors on token / userinfo**: add your site origin to the provider's allowed origins.
107
+ - **Redirect URI mismatch**: the URI registered with the provider must match the Zudoku origin
108
+ exactly, including protocol and port.
109
+ - **Missing profile fields**: ensure `profile` and `email` scopes are granted and that the provider
110
+ includes `name`, `email`, and `picture` claims in the UserInfo response.
@@ -15,8 +15,8 @@ authentication provider you use.
15
15
 
16
16
  ## Authentication Providers
17
17
 
18
- Zudoku supports Clerk, Auth0, Supabase, Firebase, Azure B2C, and any OpenID provider that supports
19
- the OpenID Connect protocol (including PingFederate).
18
+ Zudoku supports Clerk, Auth0, Supabase, Firebase, Azure B2C, and any OpenID Connect provider
19
+ (including Okta, Keycloak, Authentik, and PingFederate).
20
20
 
21
21
  Not seeing your authentication provider? [Let us know](https://github.com/zuplo/zudoku/issues)
22
22
 
@@ -96,6 +96,9 @@ When configuring your OpenID provider, you will need to set the following:
96
96
  By default, the scopes "openid", "profile", and "email" are requested. You can customize these by
97
97
  providing your own array of scopes.
98
98
 
99
+ For provider-specific guides (Okta, Keycloak, etc.), see the
100
+ [OpenID Connect setup page](./authentication-openid.md).
101
+
99
102
  ### Firebase
100
103
 
101
104
  For Firebase authentication, you will need your Firebase project configuration. You can find this in
@@ -0,0 +1,57 @@
1
+ ---
2
+ title: x-code-samples
3
+ sidebar_icon: code
4
+ ---
5
+
6
+ Use `x-code-samples` (or `x-codeSamples`) to provide custom code snippets for an API operation. When
7
+ present, these samples appear in the sidecar panel alongside the auto-generated request examples.
8
+
9
+ ## Location
10
+
11
+ The extension is added at the **Operation Object** level.
12
+
13
+ | Option | Type | Description |
14
+ | ---------------- | ---------------------- | ----------------------------- |
15
+ | `x-code-samples` | `[Code Sample Object]` | Array of custom code samples. |
16
+ | `x-codeSamples` | `[Code Sample Object]` | Alias for `x-code-samples`. |
17
+
18
+ ## Code Sample Object
19
+
20
+ | Property | Type | Required | Description |
21
+ | -------- | -------- | -------- | ---------------------------------------------------- |
22
+ | `lang` | `string` | Yes | Language identifier used for syntax highlighting. |
23
+ | `label` | `string` | No | Display label for the tab. Defaults to `lang` value. |
24
+ | `source` | `string` | Yes | The code snippet content. |
25
+
26
+ ## Example
27
+
28
+ ```yaml
29
+ paths:
30
+ /users:
31
+ get:
32
+ summary: List users
33
+ x-code-samples:
34
+ - lang: curl
35
+ label: cURL
36
+ source: |
37
+ curl -X GET https://api.example.com/users \
38
+ -H "Authorization: Bearer $TOKEN"
39
+ - lang: python
40
+ label: Python
41
+ source: |
42
+ import requests
43
+
44
+ response = requests.get(
45
+ "https://api.example.com/users",
46
+ headers={"Authorization": f"Bearer {token}"},
47
+ )
48
+ - lang: javascript
49
+ label: JavaScript
50
+ source: |
51
+ const response = await fetch("https://api.example.com/users", {
52
+ headers: { Authorization: `Bearer ${token}` },
53
+ });
54
+ responses:
55
+ "200":
56
+ description: Successful response
57
+ ```
@@ -0,0 +1,31 @@
1
+ ---
2
+ title: x-displayName
3
+ sidebar_icon: tag
4
+ ---
5
+
6
+ Use `x-displayName` to override the display label for a tag in the API navigation and documentation.
7
+ By default, Zudoku uses the tag's `name` field. This extension lets you set a different
8
+ human-friendly label without changing the tag name used for grouping operations.
9
+
10
+ ## Location
11
+
12
+ The extension is added at the **Tag Object** level.
13
+
14
+ | Option | Type | Description |
15
+ | --------------- | -------- | ------------------------------------------------------ |
16
+ | `x-displayName` | `string` | Custom display name shown in the sidebar and headings. |
17
+
18
+ ## Example
19
+
20
+ ```yaml
21
+ tags:
22
+ - name: ai-ops
23
+ description: AI-powered operations
24
+ x-displayName: AI Operations
25
+ - name: user-mgmt
26
+ description: User management endpoints
27
+ x-displayName: User Management
28
+ ```
29
+
30
+ Without `x-displayName`, the sidebar would show `ai-ops` and `user-mgmt`. With it, the sidebar
31
+ displays `AI Operations` and `User Management` instead.
@@ -0,0 +1,104 @@
1
+ ---
2
+ title: x-mcp-server
3
+ sidebar_icon: bot
4
+ ---
5
+
6
+ Use `x-mcp-server` to mark an individual OpenAPI operation as an
7
+ [MCP](https://modelcontextprotocol.io/) (Model Context Protocol) endpoint. When Zudoku detects this
8
+ extension, it replaces the standard request/response view with a dedicated MCP card showing the
9
+ endpoint URL, a copy button, and tabbed installation instructions for popular AI clients.
10
+
11
+ :::note
12
+
13
+ The `x-mcp-server` extension is applied at the **operation level** to mark specific endpoints. If
14
+ you want to describe an entire MCP server at the root level of your OpenAPI document, see the
15
+ [`x-mcp` extension](./x-mcp).
16
+
17
+ :::
18
+
19
+ ## Location
20
+
21
+ The `x-mcp-server` extension is added at the **Operation Object** level.
22
+
23
+ | Option | Type | Description |
24
+ | -------------- | -------------------------------- | ---------------------------------------------- |
25
+ | `x-mcp-server` | `boolean` or `MCP Server Object` | Marks the operation as an MCP server endpoint. |
26
+
27
+ ## MCP Server Object
28
+
29
+ When using the object form, the following properties are available:
30
+
31
+ | Property | Type | Required | Description |
32
+ | --------- | --------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------- |
33
+ | `name` | `string` | No | Display name used in the generated client configuration snippets. Falls back to the operation `summary`, then `"mcp-server"` |
34
+ | `version` | `string` | No | Version metadata |
35
+ | `tools` | `[Tool Object]` | No | Array of tools provided by the MCP server |
36
+
37
+ Each item in the `tools` array:
38
+
39
+ | Property | Type | Required | Description |
40
+ | ------------- | -------- | -------- | ------------------------------- |
41
+ | `name` | `string` | Yes | Tool name |
42
+ | `description` | `string` | No | Human-readable tool description |
43
+
44
+ ## MCP URL resolution
45
+
46
+ The displayed MCP URL is constructed from the **server URL** of the API and the **path** of the
47
+ operation. The server URL comes from the OpenAPI `servers` array (or the operation-level `servers`
48
+ override if present).
49
+
50
+ ## Examples
51
+
52
+ ### Boolean shorthand
53
+
54
+ Use `true` to enable MCP UI without specifying metadata. The operation's `summary` is used as the
55
+ server name.
56
+
57
+ ```yaml
58
+ paths:
59
+ /mcp:
60
+ post:
61
+ summary: My MCP Server
62
+ x-mcp-server: true
63
+ responses:
64
+ "200":
65
+ description: MCP response
66
+ ```
67
+
68
+ ### Object form
69
+
70
+ ```yaml
71
+ paths:
72
+ /mcp:
73
+ post:
74
+ summary: My MCP Server
75
+ x-mcp-server:
76
+ name: my-mcp-server
77
+ version: 1.0.0
78
+ tools:
79
+ - name: search_docs
80
+ description: Search the documentation
81
+ - name: get_page
82
+ description: Retrieve a specific documentation page
83
+ responses:
84
+ "200":
85
+ description: MCP response
86
+ ```
87
+
88
+ ## Generated UI
89
+
90
+ When detected, the operation page shows:
91
+
92
+ - **MCP Endpoint card** with the full URL and a copy button
93
+ - **AI Tool Configuration** tabs with setup instructions for:
94
+ - **Claude** — add via Connectors UI or `claude mcp add` CLI command
95
+ - **ChatGPT** — app setup via Settings → Apps → Advanced Settings
96
+ - **Cursor** — `mcp.json` configuration (global or project-level)
97
+ - **VS Code** — `.vscode/mcp.json` with native HTTP transport for GitHub Copilot
98
+ - **Generic** — standard `mcp.json` format compatible with most MCP clients
99
+
100
+ The standard method badge, request body, parameters, and sidecar panels are hidden for MCP
101
+ endpoints.
102
+
103
+ For a full walkthrough including Zudoku configuration, see the
104
+ [Documenting MCP Servers guide](/docs/guides/mcp-servers).