···437437 return spec;
438438};
439439440440+export const normalizeEmptyAndEnumSchemas = (spec: Record<string, unknown>) => {
441441+ const visit = (node: unknown): void => {
442442+ if (Array.isArray(node)) {
443443+ for (const item of node) {
444444+ visit(item);
445445+ }
446446+ return;
447447+ }
448448+449449+ if (!isPlainObject(node)) {
450450+ return;
451451+ }
452452+453453+ // propertyNames is not valid in OpenAPI 3.0.x — remove it
454454+ if ("propertyNames" in node) {
455455+ delete node.propertyNames;
456456+ }
457457+458458+ // Schema with enum but no type → add type: "string"
459459+ if (Array.isArray(node.enum) && !node.type && !node.$ref) {
460460+ node.type = "string";
461461+ }
462462+463463+ // $ref with siblings is invalid in 3.0.x → wrap in allOf
464464+ if (typeof node.$ref === "string" && Object.keys(node).length > 1) {
465465+ const ref = node.$ref as string;
466466+ delete node.$ref;
467467+ const rest = { ...node };
468468+ for (const k of Object.keys(node)) {
469469+ delete node[k];
470470+ }
471471+ Object.assign(node, { allOf: [{ $ref: ref }], ...rest });
472472+ }
473473+474474+ // For "properties" maps, check children for empty schemas (v.date() → {})
475475+ if (isPlainObject(node.properties)) {
476476+ const props = node.properties as Record<string, unknown>;
477477+ for (const [name, schema] of Object.entries(props)) {
478478+ if (isPlainObject(schema) && Object.keys(schema).length === 0) {
479479+ props[name] = { type: "string", format: "date-time" };
480480+ }
481481+ }
482482+ }
483483+484484+ for (const [k, value] of Object.entries(node)) {
485485+ // Replace remaining empty schemas {} (e.g. v.any() or v.unknown()).
486486+ if (isPlainObject(value) && Object.keys(value).length === 0) {
487487+ // additionalProperties: {} → true (means "any additional properties")
488488+ node[k] = k === "additionalProperties" ? true : { type: "object" };
489489+ continue;
490490+ }
491491+ visit(value);
492492+ }
493493+ };
494494+495495+ visit(spec);
496496+ return spec;
497497+};
498498+440499export const ensureOperationSummaries = (spec: Record<string, unknown>) => {
441500 const paths = ((spec as { paths?: unknown }).paths || {}) as Record<
442501 string,
+32-9
apps/docs/core/integrations/mcp.mdx
···33description: Connect Kaneo to MCP clients like Cursor and Claude Desktop using the official Kaneo MCP server.
44---
5566-Kaneo ships an official MCP server package, `@kaneo/mcp`, so you can manage workspaces, projects, tasks, comments, and labels from MCP-compatible tools.
66+Kaneo supports the Model Context Protocol (MCP) in two ways:
77+88+1. **Built-in HTTP endpoint** — every Kaneo instance exposes `/api/mcp` out of the box. No extra setup, no extra process.
99+2. **Stdio package** (`@kaneo/mcp`) — a local stdio server for clients that prefer stdio transport.
1010+1111+Both offer the same tools for managing workspaces, projects, tasks, comments, and labels.
1212+1313+## Built-in HTTP endpoint
1414+1515+Every Kaneo API instance ships with a Streamable HTTP MCP endpoint. This is the recommended approach for self-hosted deployments.
1616+1717+Point your MCP client at:
1818+1919+```
2020+https://your-kaneo-instance.com/api/mcp
2121+```
2222+2323+On first connect, you will be redirected to Kaneo's login page. After you sign in, all MCP tools run as your authenticated user.
2424+2525+The endpoint implements OAuth 2.1 with PKCE for authentication. Discovery metadata is available at:
2626+2727+- `/.well-known/oauth-protected-resource/api/mcp`
2828+- `/.well-known/oauth-authorization-server/api`
72988-The server runs locally over stdio and authenticates with Kaneo using device authorization.
3030+## Stdio package
9311010-## Prerequisites
3232+The `@kaneo/mcp` package runs a local stdio MCP server and authenticates with Kaneo using device authorization. Use this when your MCP client requires stdio transport.
3333+3434+### Prerequisites
11351236- Node.js 20 or newer
1337- A running Kaneo API
···15391640By default, self-hosted Kaneo allows both `kaneo-cli` and `kaneo-mcp` as device auth client IDs.
17411818-## Install and register
4242+### Install and register
19432044Use the recommended interactive installer:
2145···4266kaneo-mcp install --help
4367```
44684545-## Point to a self-hosted instance
6969+### Point to a self-hosted instance
46704771When generating config, pass your Kaneo base URL:
4872···59836084If you override `KANEO_MCP_CLIENT_ID`, make sure it is included in [`DEVICE_AUTH_CLIENT_IDS`](/core/installation/environment-variables).
61856262-## Authentication flow
8686+### Authentication flow
63876488On the first tool call that needs Kaneo access, the MCP server:
6589···71957296## Available tools
73977474-Current MCP tools include:
9898+Both the HTTP endpoint and stdio package expose the same tools:
759976100- Session: `whoami`, `list_workspaces`
77101- Projects: `list_projects`, `get_project`, `create_project`, `update_project`
···79103- Comments: `list_task_comments`, `create_task_comment`
80104- Labels: `list_workspace_labels`, `create_label`, `attach_label_to_task`, `detach_label_from_task`
811058282-## Run manually for debugging
106106+## Debugging
8310784108To run the stdio server directly:
85109···93117pnpm --filter @kaneo/mcp run build
94118pnpm --filter @kaneo/mcp run start
95119```
9696-
+2-1
apps/web/Dockerfile
···5252# Copy built files from builder stage
5353COPY --from=builder --chown=appuser:appuser /app/apps/web/dist /usr/share/nginx/html
54545555-# Copy nginx configuration
5555+# Copy nginx configuration (writable so env.sh can substitute placeholders at runtime)
5656COPY --chown=appuser:appuser apps/web/nginx.conf /etc/nginx/conf.d/default.conf
5757+RUN chown -R appuser:appuser /etc/nginx/conf.d
57585859# Copy and set permissions for environment script
5960COPY --chown=appuser:appuser apps/web/env.sh /docker-entrypoint.d/env.sh
+13-3
apps/web/env.sh
···66# Process KANEO_API_URL first (with special handling)
77if [ ! -z "$KANEO_API_URL" ]; then
88 echo "Found KANEO_API_URL: $KANEO_API_URL"
99-99+1010 # First, replace the exact string "KANEO_API_URL" in all JavaScript files
1111 # Use grep -l to only process files that contain the string
1212 find /usr/share/nginx/html -type f -name "*.js" -exec grep -l "KANEO_API_URL" {} \; | xargs -r sed -i "s#KANEO_API_URL#$KANEO_API_URL#g"
1313-1313+1414 # Also check for the escaped version which might appear in some files
1515 find /usr/share/nginx/html -type f -name "*.js" -exec grep -l "\"KANEO_API_URL\"" {} \; | xargs -r sed -i "s#\"KANEO_API_URL\"#\"$KANEO_API_URL\"#g"
1616-1616+1717+ # Build MCP OAuth discovery JSON for nginx to serve at /.well-known
1818+ BASE_URL=$(echo "$KANEO_API_URL" | sed 's#/api/*$##')
1919+ PRM_JSON="{\"resource\":\"${BASE_URL}/api/mcp\",\"authorization_servers\":[\"${BASE_URL}/api\"]}"
2020+ AS_JSON="{\"issuer\":\"${BASE_URL}/api\",\"authorization_endpoint\":\"${BASE_URL}/api/mcp/authorize\",\"token_endpoint\":\"${BASE_URL}/api/mcp/token\",\"registration_endpoint\":\"${BASE_URL}/api/mcp/register\",\"response_types_supported\":[\"code\"],\"grant_types_supported\":[\"authorization_code\"],\"code_challenge_methods_supported\":[\"S256\"],\"token_endpoint_auth_methods_supported\":[\"none\"]}"
2121+ sed -i "s#MCP_PRM_JSON_PLACEHOLDER#$PRM_JSON#g" /etc/nginx/conf.d/default.conf
2222+ sed -i "s#MCP_AS_JSON_PLACEHOLDER#$AS_JSON#g" /etc/nginx/conf.d/default.conf
2323+1724 echo "✅ Replaced KANEO_API_URL with $KANEO_API_URL"
1825else
1926 echo "WARNING: KANEO_API_URL environment variable is not set. API calls may fail."
2727+ # No API URL — remove MCP placeholders so nginx doesn't serve broken JSON
2828+ sed -i "s#MCP_PRM_JSON_PLACEHOLDER#{}#g" /etc/nginx/conf.d/default.conf
2929+ sed -i "s#MCP_AS_JSON_PLACEHOLDER#{}#g" /etc/nginx/conf.d/default.conf
2030fi
21312232# Process KANEO_CLIENT_URL efficiently
···11# Kaneo MCP server
2233-`@kaneo/mcp` is a local MCP server for Kaneo.
33+`@kaneo/mcp` is a local stdio MCP server for Kaneo.
4455It runs over stdio, signs in with Kaneo's device flow, and then calls the Kaneo API with a bearer token. The package lives in `packages/mcp` in this monorepo and exposes the `kaneo-mcp` CLI.
66+77+> **Tip:** Every Kaneo instance also ships a built-in HTTP MCP endpoint at `/api/mcp`. If your MCP client supports Streamable HTTP transport (e.g. Claude Code), you can connect directly without this package. See the [MCP docs](https://docs.kaneo.app/core/integrations/mcp) for details.
6879## Prerequisites
810