From 81bfe8f1b886fe510d7f5e1eec1e776ff84210e7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 31 Mar 2026 05:05:15 +0000 Subject: [PATCH 1/4] Add opt-in saved app search visibility Co-authored-by: Kent C. Dodds --- docs/agents/mcp-apps-spec-notes.md | 3 + .../0013-ui-artifact-search-visibility.sql | 2 + .../src/app/saved-ui-hosted-html.node.test.ts | 2 + .../src/mcp/capabilities/apps/ui-get-app.ts | 2 + .../src/mcp/capabilities/apps/ui-list-apps.ts | 2 + .../capabilities/apps/ui-load-app-source.ts | 2 + .../src/mcp/capabilities/apps/ui-save-app.ts | 42 +++++-- .../unified-search.workers.test.ts | 1 + packages/worker/src/mcp/index.ts | 2 +- .../worker/src/mcp/mcp-server.mcp-e2e.test.ts | 107 ++++++++++++++++++ .../src/mcp/observability.workers.test.ts | 6 +- .../worker/src/mcp/tools/search.node.test.ts | 1 + packages/worker/src/mcp/tools/search.ts | 4 +- packages/worker/src/mcp/ui-artifacts-repo.ts | 41 +++++-- packages/worker/src/mcp/ui-artifacts-types.ts | 1 + 15 files changed, 195 insertions(+), 23 deletions(-) create mode 100644 packages/worker/migrations/0013-ui-artifact-search-visibility.sql diff --git a/docs/agents/mcp-apps-spec-notes.md b/docs/agents/mcp-apps-spec-notes.md index 51e61633f2..0826228910 100644 --- a/docs/agents/mcp-apps-spec-notes.md +++ b/docs/agents/mcp-apps-spec-notes.md @@ -15,6 +15,9 @@ over project-local conventions. - The repo exposes a single generic shell via `open_generated_ui`. - Saved apps are reopened by `app_id`; inline renders are ephemeral. +- Saved apps are hidden from `search` by default; set + `include_in_search_results: true` in `ui_save_app` only for reusable apps that + should be discoverable. - If an OAuth provider requires a callback URL, use a persisted hosted saved app rather than an inline render. - For secret-bearing requests and host approval policy, also read diff --git a/packages/worker/migrations/0013-ui-artifact-search-visibility.sql b/packages/worker/migrations/0013-ui-artifact-search-visibility.sql new file mode 100644 index 0000000000..207699ef3b --- /dev/null +++ b/packages/worker/migrations/0013-ui-artifact-search-visibility.sql @@ -0,0 +1,2 @@ +ALTER TABLE ui_artifacts +ADD COLUMN include_in_search_results INTEGER NOT NULL DEFAULT 0; diff --git a/packages/worker/src/app/saved-ui-hosted-html.node.test.ts b/packages/worker/src/app/saved-ui-hosted-html.node.test.ts index fa7186b840..aab7a2122c 100644 --- a/packages/worker/src/app/saved-ui-hosted-html.node.test.ts +++ b/packages/worker/src/app/saved-ui-hosted-html.node.test.ts @@ -23,6 +23,7 @@ test('renderHostedSavedUiHtml emits shared runtime assets for html apps', () => code: '
Hello
', runtime: 'html', parameters: null, + include_in_search_results: false, created_at: '2026-03-27T00:00:00.000Z', updated_at: '2026-03-27T00:00:00.000Z', }, @@ -80,6 +81,7 @@ test('renderHostedSavedUiHtml keeps user javascript separate from runtime bootst code: 'document.querySelector("[data-generated-ui-root]")?.append("hello")', runtime: 'javascript', parameters: null, + include_in_search_results: false, created_at: '2026-03-27T00:00:00.000Z', updated_at: '2026-03-27T00:00:00.000Z', }, diff --git a/packages/worker/src/mcp/capabilities/apps/ui-get-app.ts b/packages/worker/src/mcp/capabilities/apps/ui-get-app.ts index c99de05700..a49e5a9ad3 100644 --- a/packages/worker/src/mcp/capabilities/apps/ui-get-app.ts +++ b/packages/worker/src/mcp/capabilities/apps/ui-get-app.ts @@ -14,6 +14,7 @@ const outputSchema = z.object({ title: z.string(), description: z.string(), parameters: z.array(uiArtifactParameterSchema).nullable(), + include_in_search_results: z.boolean(), code: z .string() .describe('Generated UI source code to render inside the generic shell.'), @@ -54,6 +55,7 @@ export const uiGetAppCapability = defineDomainCapability( title: row.title, description: row.description, parameters: parseUiArtifactParameters(row.parameters), + include_in_search_results: row.include_in_search_results, code: row.code, runtime: row.runtime, created_at: row.created_at, diff --git a/packages/worker/src/mcp/capabilities/apps/ui-list-apps.ts b/packages/worker/src/mcp/capabilities/apps/ui-list-apps.ts index d9f077a31c..7bfac1960c 100644 --- a/packages/worker/src/mcp/capabilities/apps/ui-list-apps.ts +++ b/packages/worker/src/mcp/capabilities/apps/ui-list-apps.ts @@ -17,6 +17,7 @@ const outputSchema = z.object({ description: z.string(), runtime: z.string(), parameters: z.array(uiArtifactParameterSchema).nullable(), + include_in_search_results: z.boolean(), created_at: z.string(), updated_at: z.string(), }), @@ -45,6 +46,7 @@ export const uiListAppsCapability = defineDomainCapability( description: row.description, runtime: row.runtime, parameters: parseUiArtifactParameters(row.parameters), + include_in_search_results: row.include_in_search_results, created_at: row.created_at, updated_at: row.updated_at, })), diff --git a/packages/worker/src/mcp/capabilities/apps/ui-load-app-source.ts b/packages/worker/src/mcp/capabilities/apps/ui-load-app-source.ts index 7849d786cc..ae576ac09a 100644 --- a/packages/worker/src/mcp/capabilities/apps/ui-load-app-source.ts +++ b/packages/worker/src/mcp/capabilities/apps/ui-load-app-source.ts @@ -16,6 +16,7 @@ const outputSchema = z.object({ runtime: z.enum(['html', 'javascript']), code: z.string(), parameters: z.array(uiArtifactParameterSchema).nullable(), + include_in_search_results: z.boolean(), }) export const uiLoadAppSourceCapability = defineDomainCapability( @@ -52,6 +53,7 @@ export const uiLoadAppSourceCapability = defineDomainCapability( runtime: row.runtime, code: row.code, parameters: parseUiArtifactParameters(row.parameters), + include_in_search_results: row.include_in_search_results, } }, }, diff --git a/packages/worker/src/mcp/capabilities/apps/ui-save-app.ts b/packages/worker/src/mcp/capabilities/apps/ui-save-app.ts index f9b1ee9fc8..22aed7f1ab 100644 --- a/packages/worker/src/mcp/capabilities/apps/ui-save-app.ts +++ b/packages/worker/src/mcp/capabilities/apps/ui-save-app.ts @@ -10,7 +10,10 @@ import { updateUiArtifact, } from '#mcp/ui-artifacts-repo.ts' import { buildUiArtifactEmbedText } from '#mcp/ui-artifacts-embed.ts' -import { upsertUiArtifactVector } from '#mcp/ui-artifacts-vectorize.ts' +import { + deleteUiArtifactVector, + upsertUiArtifactVector, +} from '#mcp/ui-artifacts-vectorize.ts' import { requireMcpUser } from '#mcp/capabilities/meta/require-user.ts' import { normalizeUiArtifactParameters, @@ -48,6 +51,12 @@ const inputSchema = z.object({ .describe( 'Optional parameter definitions for reusable saved apps. Resolved values are exposed at runtime on the imported `kodyWidget.params` helper from `@kody/ui-utils`.', ), + include_in_search_results: z + .boolean() + .optional() + .describe( + 'Whether this saved app should appear in search results. Defaults to false so one-off apps stay hidden unless explicitly opted in.', + ), }) const outputSchema = z.object({ @@ -55,6 +64,7 @@ const outputSchema = z.object({ runtime: z.enum(['html', 'javascript']), hosted_url: z.string().url(), parameters: z.array(uiArtifactParameterSchema).nullable(), + include_in_search_results: z.boolean(), }) export const uiSaveAppCapability = defineDomainCapability( @@ -74,6 +84,7 @@ export const uiSaveAppCapability = defineDomainCapability( const isUpdate = args.app_id !== undefined const appId = args.app_id ?? crypto.randomUUID() const parameters = normalizeUiArtifactParameters(args.parameters) + const includeInSearchResults = args.include_in_search_results ?? false const serializedParameters = parameters ? JSON.stringify(parameters) : null @@ -89,6 +100,7 @@ export const uiSaveAppCapability = defineDomainCapability( code: args.code, runtime: args.runtime, parameters: serializedParameters, + include_in_search_results: includeInSearchResults, }, ) if (!updated) { @@ -104,23 +116,28 @@ export const uiSaveAppCapability = defineDomainCapability( code: args.code, runtime: args.runtime, parameters: serializedParameters, + include_in_search_results: includeInSearchResults, created_at: now, updated_at: now, }) } try { - await upsertUiArtifactVector(ctx.env, { - appId, - userId: user.userId, - embedText: buildUiArtifactEmbedText({ - title: args.title, - description: args.description, - code: args.code, - runtime: args.runtime, - parameters, - }), - }) + if (includeInSearchResults) { + await upsertUiArtifactVector(ctx.env, { + appId, + userId: user.userId, + embedText: buildUiArtifactEmbedText({ + title: args.title, + description: args.description, + code: args.code, + runtime: args.runtime, + parameters, + }), + }) + } else { + await deleteUiArtifactVector(ctx.env, appId) + } } catch (cause) { if (!isUpdate) { await deleteUiArtifact(ctx.env.APP_DB, user.userId, appId) @@ -156,6 +173,7 @@ export const uiSaveAppCapability = defineDomainCapability( runtime: args.runtime, hosted_url: buildSavedUiUrl(ctx.callerContext.baseUrl, appId), parameters, + include_in_search_results: includeInSearchResults, } }, }, diff --git a/packages/worker/src/mcp/capabilities/unified-search.workers.test.ts b/packages/worker/src/mcp/capabilities/unified-search.workers.test.ts index b88fef634a..46685a14d1 100644 --- a/packages/worker/src/mcp/capabilities/unified-search.workers.test.ts +++ b/packages/worker/src/mcp/capabilities/unified-search.workers.test.ts @@ -43,6 +43,7 @@ function createUiArtifactRow(appId: string): UiArtifactRow { required: true, }, ]), + include_in_search_results: true, created_at: '2026-03-20T00:00:00.000Z', updated_at: '2026-03-20T00:00:00.000Z', } diff --git a/packages/worker/src/mcp/index.ts b/packages/worker/src/mcp/index.ts index eb4ec888a4..301332a8dc 100644 --- a/packages/worker/src/mcp/index.ts +++ b/packages/worker/src/mcp/index.ts @@ -42,7 +42,7 @@ Quick start - Never ask the user to paste secrets, tokens, API keys, passwords, OAuth codes, or client secrets into chat. Use saved secrets when available, or use 'open_generated_ui' to collect and save sensitive values instead. - Use 'meta_save_skill' only for workflows that are reasonably repeatable—patterns you expect to run again with similar structure or inputs. Do not save one-off tasks, unique ad-hoc work, or highly bespoke requests as skills; run those with 'execute' instead. Use the optional 'collection' field to group related saved skills, and use 'meta_update_skill' to replace an existing skill's code in place. - When a saved skill declares parameters, pass values via meta_run_skill params; the codemode can read them from the params variable. -- Use 'ui_save_app' to persist reusable UI source for later reopening via 'app_id'. Saved apps are user-scoped UI artifacts, not codemode skills. +- Use 'ui_save_app' to persist reusable UI source for later reopening via 'app_id'. Saved apps are user-scoped UI artifacts, not codemode skills. They are hidden from search by default unless you explicitly set \`include_in_search_results: true\` for reusable apps. - Use \`codemode.secret_list(args)\` during execute-time code to list secret metadata only; it does not return plaintext values. - Use \`codemode.secret_set(args)\` only to persist secret values that are already available inside trusted execution, such as refreshed OAuth tokens. It returns metadata only and never returns plaintext values. diff --git a/packages/worker/src/mcp/mcp-server.mcp-e2e.test.ts b/packages/worker/src/mcp/mcp-server.mcp-e2e.test.ts index 6edbc5f9b9..00f4438275 100644 --- a/packages/worker/src/mcp/mcp-server.mcp-e2e.test.ts +++ b/packages/worker/src/mcp/mcp-server.mcp-e2e.test.ts @@ -232,6 +232,113 @@ test('mcp server executes user code against codemode and tracks execute context' expect(executeResult?.hosted_url).toBe( `${server.origin}/ui/${executeResult?.app_id}`, ) + expect(executeResult?.include_in_search_results).toBe(false) + + const hiddenSearchResult = await mcpClient.client.callTool({ + name: 'search', + arguments: { + query: 'Execute generated app', + detail: true, + }, + }) + const hiddenSearchStructured = (hiddenSearchResult as CallToolResult) + .structuredContent as + | { + result?: { + matches?: Array<{ + type?: string + appId?: string + }> + } + } + | undefined + expect( + hiddenSearchStructured?.result?.matches?.some( + (match) => + match.type === 'app' && match.appId === executeResult?.app_id, + ), + ).toBe(false) + + const savedAppMetadata = await mcpClient.client.callTool({ + name: 'execute', + arguments: { + code: `async () => { + return await codemode.ui_get_app({ + app_id: ${JSON.stringify(executeResult?.app_id)}, + }) + }`, + }, + }) + const savedAppMetadataStructured = (savedAppMetadata as CallToolResult) + .structuredContent as + | { + result?: { + include_in_search_results?: boolean + } + } + | undefined + expect(savedAppMetadataStructured?.result?.include_in_search_results).toBe( + false, + ) + + const searchableUpdateResult = await mcpClient.client.callTool({ + name: 'execute', + arguments: { + code: `async () => { + return await codemode.ui_save_app({ + app_id: ${JSON.stringify(executeResult?.app_id)}, + title: 'Execute generated app', + description: 'Saved through execute.', + code: '

Execute App

', + include_in_search_results: true, + }) + }`, + }, + }) + const searchableUpdateStructured = ( + searchableUpdateResult as CallToolResult + ).structuredContent as + | { + result?: { + include_in_search_results?: boolean + } + } + | undefined + expect(searchableUpdateStructured?.result?.include_in_search_results).toBe( + true, + ) + + const visibleSearchResult = await mcpClient.client.callTool({ + name: 'search', + arguments: { + query: 'Execute generated app', + detail: true, + limit: 20, + maxResponseSize: 20_000, + }, + }) + const visibleSearchStructured = (visibleSearchResult as CallToolResult) + .structuredContent as + | { + result?: { + matches?: Array<{ + type?: string + appId?: string + }> + } + } + | undefined + expect( + visibleSearchStructured?.result?.matches?.find( + (match) => + match.type === 'app' && match.appId === executeResult?.app_id, + ), + ).toEqual( + expect.objectContaining({ + type: 'app', + appId: executeResult?.app_id, + }), + ) const contextResult = await mcpClient.client.callTool({ name: 'execute', diff --git a/packages/worker/src/mcp/observability.workers.test.ts b/packages/worker/src/mcp/observability.workers.test.ts index b6a432f17b..656c975665 100644 --- a/packages/worker/src/mcp/observability.workers.test.ts +++ b/packages/worker/src/mcp/observability.workers.test.ts @@ -146,7 +146,6 @@ test('ui_save_app capability logs success for valid invocation', async () => { { title: 'Observed app', description: 'Observation test app.', - keywords: ['observability'], code: 'document.querySelector("#app")!.innerHTML = "

Observed app

"', }, { @@ -175,6 +174,10 @@ test('ui_save_app capability logs success for valid invocation', async () => { }, ) expect(typeof (result as { app_id: string }).app_id).toBe('string') + expect( + (result as { include_in_search_results: boolean }) + .include_in_search_results, + ).toBe(false) } finally { console.info = originalInfo } @@ -201,6 +204,7 @@ test('ui_save_app logs vector refresh failure for in-place updates and still suc title: 'Observed app', description: 'Observation test app.', code: '

Observed app

', + include_in_search_results: true, }, { env: { diff --git a/packages/worker/src/mcp/tools/search.node.test.ts b/packages/worker/src/mcp/tools/search.node.test.ts index 4a766783fa..11444134da 100644 --- a/packages/worker/src/mcp/tools/search.node.test.ts +++ b/packages/worker/src/mcp/tools/search.node.test.ts @@ -19,6 +19,7 @@ test('optional search rows fall back when saved skills lookup fails', async () = code: '
', runtime: 'html', parameters: null, + include_in_search_results: true, created_at: '2026-03-24T00:00:00.000Z', updated_at: '2026-03-24T00:00:00.000Z', }, diff --git a/packages/worker/src/mcp/tools/search.ts b/packages/worker/src/mcp/tools/search.ts index c4085c880a..63f2eaab92 100644 --- a/packages/worker/src/mcp/tools/search.ts +++ b/packages/worker/src/mcp/tools/search.ts @@ -269,7 +269,9 @@ export async function registerSearchTool(agent: McpRegistrationAgent) { loadSkills: () => listMcpSkillsByUserId(agent.getEnv().APP_DB, userId!), loadUiArtifacts: () => - listUiArtifactsByUserId(agent.getEnv().APP_DB, userId!), + listUiArtifactsByUserId(agent.getEnv().APP_DB, userId!, { + includeInSearchResults: true, + }), loadUserSecrets: () => listUserSecretsForSearch({ env: agent.getEnv(), diff --git a/packages/worker/src/mcp/ui-artifacts-repo.ts b/packages/worker/src/mcp/ui-artifacts-repo.ts index c1e2cbdcd2..ac8d500d90 100644 --- a/packages/worker/src/mcp/ui-artifacts-repo.ts +++ b/packages/worker/src/mcp/ui-artifacts-repo.ts @@ -16,8 +16,8 @@ export async function insertUiArtifact( .prepare( `INSERT INTO ui_artifacts ( id, user_id, title, description, source_code, source_type, - parameters, created_at, updated_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + parameters, include_in_search_results, created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, ) .bind( row.id, @@ -27,6 +27,7 @@ export async function insertUiArtifact( row.code, row.runtime, row.parameters ?? null, + row.include_in_search_results ? 1 : 0, row.created_at ?? now, row.updated_at ?? now, ) @@ -41,7 +42,7 @@ export async function getUiArtifactById( const result = await db .prepare( `SELECT id, user_id, title, description, source_code, source_type, - parameters, created_at, updated_at + parameters, include_in_search_results, created_at, updated_at FROM ui_artifacts WHERE id = ? AND user_id = ?`, ) .bind(artifactId, userId) @@ -62,7 +63,7 @@ export async function getUiArtifactByOwnerIds( const result = await db .prepare( `SELECT id, user_id, title, description, source_code, source_type, - parameters, created_at, updated_at + parameters, include_in_search_results, created_at, updated_at FROM ui_artifacts WHERE id = ? AND user_id IN (${placeholders}) LIMIT 1`, @@ -92,7 +93,12 @@ export async function updateUiArtifact( updates: Partial< Pick< UiArtifactRow, - 'title' | 'description' | 'code' | 'runtime' | 'parameters' + | 'title' + | 'description' + | 'code' + | 'runtime' + | 'parameters' + | 'include_in_search_results' > >, ): Promise { @@ -118,6 +124,12 @@ export async function updateUiArtifact( if (updates.parameters !== undefined) { addAssignment('parameters', updates.parameters ?? null) } + if (updates.include_in_search_results !== undefined) { + addAssignment( + 'include_in_search_results', + updates.include_in_search_results ? 1 : 0, + ) + } addAssignment('updated_at', new Date().toISOString()) @@ -133,14 +145,23 @@ export async function updateUiArtifact( export async function listUiArtifactsByUserId( db: D1Database, userId: string, + options?: { includeInSearchResults?: boolean }, ): Promise> { + const includeInSearchResults = options?.includeInSearchResults const { results } = await db .prepare( `SELECT id, user_id, title, description, source_code, source_type, - parameters, created_at, updated_at - FROM ui_artifacts WHERE user_id = ?`, + parameters, include_in_search_results, created_at, updated_at + FROM ui_artifacts + WHERE user_id = ? + ${includeInSearchResults === undefined ? '' : 'AND include_in_search_results = ?'}`, + ) + .bind( + userId, + ...(includeInSearchResults === undefined + ? [] + : [includeInSearchResults ? 1 : 0]), ) - .bind(userId) .all>() return (results ?? []).map(mapRow) } @@ -154,6 +175,10 @@ function mapRow(row: Record): UiArtifactRow { code: String(row['source_code']), runtime: String(row['source_type']) as UiArtifactRow['runtime'], parameters: row['parameters'] == null ? null : String(row['parameters']), + include_in_search_results: + row['include_in_search_results'] === 1 || + row['include_in_search_results'] === '1' || + row['include_in_search_results'] === true, created_at: String(row['created_at']), updated_at: String(row['updated_at']), } diff --git a/packages/worker/src/mcp/ui-artifacts-types.ts b/packages/worker/src/mcp/ui-artifacts-types.ts index e8d4640ad4..e6462ae7b3 100644 --- a/packages/worker/src/mcp/ui-artifacts-types.ts +++ b/packages/worker/src/mcp/ui-artifacts-types.ts @@ -8,6 +8,7 @@ export type UiArtifactRow = { code: string runtime: UiArtifactRuntime parameters: string | null + include_in_search_results: boolean created_at: string updated_at: string } From 21a3cf5cd4bc077d315cdae853b0a115cd58e4b9 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 31 Mar 2026 05:46:03 +0000 Subject: [PATCH 2/4] Rename saved app visibility flag to hidden Co-authored-by: Kent C. Dodds --- docs/agents/mcp-apps-spec-notes.md | 2 +- .../0013-ui-artifact-search-visibility.sql | 2 +- .../src/app/saved-ui-hosted-html.node.test.ts | 4 +- .../src/mcp/capabilities/apps/ui-get-app.ts | 4 +- .../src/mcp/capabilities/apps/ui-list-apps.ts | 4 +- .../capabilities/apps/ui-load-app-source.ts | 4 +- .../src/mcp/capabilities/apps/ui-save-app.ts | 16 ++++---- .../unified-search.workers.test.ts | 2 +- packages/worker/src/mcp/index.ts | 2 +- .../worker/src/mcp/mcp-server.mcp-e2e.test.ts | 16 +++----- .../src/mcp/observability.workers.test.ts | 7 +--- .../worker/src/mcp/tools/search.node.test.ts | 2 +- packages/worker/src/mcp/tools/search.ts | 2 +- packages/worker/src/mcp/ui-artifacts-repo.ts | 37 ++++++++----------- packages/worker/src/mcp/ui-artifacts-types.ts | 2 +- 15 files changed, 47 insertions(+), 59 deletions(-) diff --git a/docs/agents/mcp-apps-spec-notes.md b/docs/agents/mcp-apps-spec-notes.md index 0826228910..b481f69af9 100644 --- a/docs/agents/mcp-apps-spec-notes.md +++ b/docs/agents/mcp-apps-spec-notes.md @@ -16,7 +16,7 @@ over project-local conventions. - The repo exposes a single generic shell via `open_generated_ui`. - Saved apps are reopened by `app_id`; inline renders are ephemeral. - Saved apps are hidden from `search` by default; set - `include_in_search_results: true` in `ui_save_app` only for reusable apps that + `hidden: false` in `ui_save_app` only for reusable apps that should be discoverable. - If an OAuth provider requires a callback URL, use a persisted hosted saved app rather than an inline render. diff --git a/packages/worker/migrations/0013-ui-artifact-search-visibility.sql b/packages/worker/migrations/0013-ui-artifact-search-visibility.sql index 207699ef3b..400fa6ebeb 100644 --- a/packages/worker/migrations/0013-ui-artifact-search-visibility.sql +++ b/packages/worker/migrations/0013-ui-artifact-search-visibility.sql @@ -1,2 +1,2 @@ ALTER TABLE ui_artifacts -ADD COLUMN include_in_search_results INTEGER NOT NULL DEFAULT 0; +ADD COLUMN hidden INTEGER NOT NULL DEFAULT 1; diff --git a/packages/worker/src/app/saved-ui-hosted-html.node.test.ts b/packages/worker/src/app/saved-ui-hosted-html.node.test.ts index aab7a2122c..b16c08c41c 100644 --- a/packages/worker/src/app/saved-ui-hosted-html.node.test.ts +++ b/packages/worker/src/app/saved-ui-hosted-html.node.test.ts @@ -23,7 +23,7 @@ test('renderHostedSavedUiHtml emits shared runtime assets for html apps', () => code: '
Hello
', runtime: 'html', parameters: null, - include_in_search_results: false, + hidden: true, created_at: '2026-03-27T00:00:00.000Z', updated_at: '2026-03-27T00:00:00.000Z', }, @@ -81,7 +81,7 @@ test('renderHostedSavedUiHtml keeps user javascript separate from runtime bootst code: 'document.querySelector("[data-generated-ui-root]")?.append("hello")', runtime: 'javascript', parameters: null, - include_in_search_results: false, + hidden: true, created_at: '2026-03-27T00:00:00.000Z', updated_at: '2026-03-27T00:00:00.000Z', }, diff --git a/packages/worker/src/mcp/capabilities/apps/ui-get-app.ts b/packages/worker/src/mcp/capabilities/apps/ui-get-app.ts index a49e5a9ad3..3a8b10b17d 100644 --- a/packages/worker/src/mcp/capabilities/apps/ui-get-app.ts +++ b/packages/worker/src/mcp/capabilities/apps/ui-get-app.ts @@ -14,7 +14,7 @@ const outputSchema = z.object({ title: z.string(), description: z.string(), parameters: z.array(uiArtifactParameterSchema).nullable(), - include_in_search_results: z.boolean(), + hidden: z.boolean(), code: z .string() .describe('Generated UI source code to render inside the generic shell.'), @@ -55,7 +55,7 @@ export const uiGetAppCapability = defineDomainCapability( title: row.title, description: row.description, parameters: parseUiArtifactParameters(row.parameters), - include_in_search_results: row.include_in_search_results, + hidden: row.hidden, code: row.code, runtime: row.runtime, created_at: row.created_at, diff --git a/packages/worker/src/mcp/capabilities/apps/ui-list-apps.ts b/packages/worker/src/mcp/capabilities/apps/ui-list-apps.ts index 7bfac1960c..968f01ab45 100644 --- a/packages/worker/src/mcp/capabilities/apps/ui-list-apps.ts +++ b/packages/worker/src/mcp/capabilities/apps/ui-list-apps.ts @@ -17,7 +17,7 @@ const outputSchema = z.object({ description: z.string(), runtime: z.string(), parameters: z.array(uiArtifactParameterSchema).nullable(), - include_in_search_results: z.boolean(), + hidden: z.boolean(), created_at: z.string(), updated_at: z.string(), }), @@ -46,7 +46,7 @@ export const uiListAppsCapability = defineDomainCapability( description: row.description, runtime: row.runtime, parameters: parseUiArtifactParameters(row.parameters), - include_in_search_results: row.include_in_search_results, + hidden: row.hidden, created_at: row.created_at, updated_at: row.updated_at, })), diff --git a/packages/worker/src/mcp/capabilities/apps/ui-load-app-source.ts b/packages/worker/src/mcp/capabilities/apps/ui-load-app-source.ts index ae576ac09a..657a786069 100644 --- a/packages/worker/src/mcp/capabilities/apps/ui-load-app-source.ts +++ b/packages/worker/src/mcp/capabilities/apps/ui-load-app-source.ts @@ -16,7 +16,7 @@ const outputSchema = z.object({ runtime: z.enum(['html', 'javascript']), code: z.string(), parameters: z.array(uiArtifactParameterSchema).nullable(), - include_in_search_results: z.boolean(), + hidden: z.boolean(), }) export const uiLoadAppSourceCapability = defineDomainCapability( @@ -53,7 +53,7 @@ export const uiLoadAppSourceCapability = defineDomainCapability( runtime: row.runtime, code: row.code, parameters: parseUiArtifactParameters(row.parameters), - include_in_search_results: row.include_in_search_results, + hidden: row.hidden, } }, }, diff --git a/packages/worker/src/mcp/capabilities/apps/ui-save-app.ts b/packages/worker/src/mcp/capabilities/apps/ui-save-app.ts index 22aed7f1ab..b483f96da6 100644 --- a/packages/worker/src/mcp/capabilities/apps/ui-save-app.ts +++ b/packages/worker/src/mcp/capabilities/apps/ui-save-app.ts @@ -51,11 +51,11 @@ const inputSchema = z.object({ .describe( 'Optional parameter definitions for reusable saved apps. Resolved values are exposed at runtime on the imported `kodyWidget.params` helper from `@kody/ui-utils`.', ), - include_in_search_results: z + hidden: z .boolean() .optional() .describe( - 'Whether this saved app should appear in search results. Defaults to false so one-off apps stay hidden unless explicitly opted in.', + 'Whether this saved app should stay hidden from search results. Defaults to true so one-off apps stay private unless explicitly made discoverable.', ), }) @@ -64,7 +64,7 @@ const outputSchema = z.object({ runtime: z.enum(['html', 'javascript']), hosted_url: z.string().url(), parameters: z.array(uiArtifactParameterSchema).nullable(), - include_in_search_results: z.boolean(), + hidden: z.boolean(), }) export const uiSaveAppCapability = defineDomainCapability( @@ -84,7 +84,7 @@ export const uiSaveAppCapability = defineDomainCapability( const isUpdate = args.app_id !== undefined const appId = args.app_id ?? crypto.randomUUID() const parameters = normalizeUiArtifactParameters(args.parameters) - const includeInSearchResults = args.include_in_search_results ?? false + const hidden = args.hidden ?? true const serializedParameters = parameters ? JSON.stringify(parameters) : null @@ -100,7 +100,7 @@ export const uiSaveAppCapability = defineDomainCapability( code: args.code, runtime: args.runtime, parameters: serializedParameters, - include_in_search_results: includeInSearchResults, + hidden, }, ) if (!updated) { @@ -116,14 +116,14 @@ export const uiSaveAppCapability = defineDomainCapability( code: args.code, runtime: args.runtime, parameters: serializedParameters, - include_in_search_results: includeInSearchResults, + hidden, created_at: now, updated_at: now, }) } try { - if (includeInSearchResults) { + if (!hidden) { await upsertUiArtifactVector(ctx.env, { appId, userId: user.userId, @@ -173,7 +173,7 @@ export const uiSaveAppCapability = defineDomainCapability( runtime: args.runtime, hosted_url: buildSavedUiUrl(ctx.callerContext.baseUrl, appId), parameters, - include_in_search_results: includeInSearchResults, + hidden, } }, }, diff --git a/packages/worker/src/mcp/capabilities/unified-search.workers.test.ts b/packages/worker/src/mcp/capabilities/unified-search.workers.test.ts index 46685a14d1..b255692c6f 100644 --- a/packages/worker/src/mcp/capabilities/unified-search.workers.test.ts +++ b/packages/worker/src/mcp/capabilities/unified-search.workers.test.ts @@ -43,7 +43,7 @@ function createUiArtifactRow(appId: string): UiArtifactRow { required: true, }, ]), - include_in_search_results: true, + hidden: false, created_at: '2026-03-20T00:00:00.000Z', updated_at: '2026-03-20T00:00:00.000Z', } diff --git a/packages/worker/src/mcp/index.ts b/packages/worker/src/mcp/index.ts index 301332a8dc..f952d618ae 100644 --- a/packages/worker/src/mcp/index.ts +++ b/packages/worker/src/mcp/index.ts @@ -42,7 +42,7 @@ Quick start - Never ask the user to paste secrets, tokens, API keys, passwords, OAuth codes, or client secrets into chat. Use saved secrets when available, or use 'open_generated_ui' to collect and save sensitive values instead. - Use 'meta_save_skill' only for workflows that are reasonably repeatable—patterns you expect to run again with similar structure or inputs. Do not save one-off tasks, unique ad-hoc work, or highly bespoke requests as skills; run those with 'execute' instead. Use the optional 'collection' field to group related saved skills, and use 'meta_update_skill' to replace an existing skill's code in place. - When a saved skill declares parameters, pass values via meta_run_skill params; the codemode can read them from the params variable. -- Use 'ui_save_app' to persist reusable UI source for later reopening via 'app_id'. Saved apps are user-scoped UI artifacts, not codemode skills. They are hidden from search by default unless you explicitly set \`include_in_search_results: true\` for reusable apps. + - Use 'ui_save_app' to persist reusable UI source for later reopening via 'app_id'. Saved apps are user-scoped UI artifacts, not codemode skills. They are hidden from search by default unless you explicitly set \`hidden: false\` for reusable apps. - Use \`codemode.secret_list(args)\` during execute-time code to list secret metadata only; it does not return plaintext values. - Use \`codemode.secret_set(args)\` only to persist secret values that are already available inside trusted execution, such as refreshed OAuth tokens. It returns metadata only and never returns plaintext values. diff --git a/packages/worker/src/mcp/mcp-server.mcp-e2e.test.ts b/packages/worker/src/mcp/mcp-server.mcp-e2e.test.ts index 00f4438275..2738f69ef1 100644 --- a/packages/worker/src/mcp/mcp-server.mcp-e2e.test.ts +++ b/packages/worker/src/mcp/mcp-server.mcp-e2e.test.ts @@ -232,7 +232,7 @@ test('mcp server executes user code against codemode and tracks execute context' expect(executeResult?.hosted_url).toBe( `${server.origin}/ui/${executeResult?.app_id}`, ) - expect(executeResult?.include_in_search_results).toBe(false) + expect(executeResult?.hidden).toBe(true) const hiddenSearchResult = await mcpClient.client.callTool({ name: 'search', @@ -273,13 +273,11 @@ test('mcp server executes user code against codemode and tracks execute context' .structuredContent as | { result?: { - include_in_search_results?: boolean + hidden?: boolean } } | undefined - expect(savedAppMetadataStructured?.result?.include_in_search_results).toBe( - false, - ) + expect(savedAppMetadataStructured?.result?.hidden).toBe(true) const searchableUpdateResult = await mcpClient.client.callTool({ name: 'execute', @@ -290,7 +288,7 @@ test('mcp server executes user code against codemode and tracks execute context' title: 'Execute generated app', description: 'Saved through execute.', code: '

Execute App

', - include_in_search_results: true, + hidden: false, }) }`, }, @@ -300,13 +298,11 @@ test('mcp server executes user code against codemode and tracks execute context' ).structuredContent as | { result?: { - include_in_search_results?: boolean + hidden?: boolean } } | undefined - expect(searchableUpdateStructured?.result?.include_in_search_results).toBe( - true, - ) + expect(searchableUpdateStructured?.result?.hidden).toBe(false) const visibleSearchResult = await mcpClient.client.callTool({ name: 'search', diff --git a/packages/worker/src/mcp/observability.workers.test.ts b/packages/worker/src/mcp/observability.workers.test.ts index 656c975665..dbc88ccc62 100644 --- a/packages/worker/src/mcp/observability.workers.test.ts +++ b/packages/worker/src/mcp/observability.workers.test.ts @@ -174,10 +174,7 @@ test('ui_save_app capability logs success for valid invocation', async () => { }, ) expect(typeof (result as { app_id: string }).app_id).toBe('string') - expect( - (result as { include_in_search_results: boolean }) - .include_in_search_results, - ).toBe(false) + expect((result as { hidden: boolean }).hidden).toBe(true) } finally { console.info = originalInfo } @@ -204,7 +201,7 @@ test('ui_save_app logs vector refresh failure for in-place updates and still suc title: 'Observed app', description: 'Observation test app.', code: '

Observed app

', - include_in_search_results: true, + hidden: false, }, { env: { diff --git a/packages/worker/src/mcp/tools/search.node.test.ts b/packages/worker/src/mcp/tools/search.node.test.ts index 11444134da..559bbc796c 100644 --- a/packages/worker/src/mcp/tools/search.node.test.ts +++ b/packages/worker/src/mcp/tools/search.node.test.ts @@ -19,7 +19,7 @@ test('optional search rows fall back when saved skills lookup fails', async () = code: '
', runtime: 'html', parameters: null, - include_in_search_results: true, + hidden: false, created_at: '2026-03-24T00:00:00.000Z', updated_at: '2026-03-24T00:00:00.000Z', }, diff --git a/packages/worker/src/mcp/tools/search.ts b/packages/worker/src/mcp/tools/search.ts index 63f2eaab92..092c8b372d 100644 --- a/packages/worker/src/mcp/tools/search.ts +++ b/packages/worker/src/mcp/tools/search.ts @@ -270,7 +270,7 @@ export async function registerSearchTool(agent: McpRegistrationAgent) { listMcpSkillsByUserId(agent.getEnv().APP_DB, userId!), loadUiArtifacts: () => listUiArtifactsByUserId(agent.getEnv().APP_DB, userId!, { - includeInSearchResults: true, + hidden: false, }), loadUserSecrets: () => listUserSecretsForSearch({ diff --git a/packages/worker/src/mcp/ui-artifacts-repo.ts b/packages/worker/src/mcp/ui-artifacts-repo.ts index ac8d500d90..c872a649e8 100644 --- a/packages/worker/src/mcp/ui-artifacts-repo.ts +++ b/packages/worker/src/mcp/ui-artifacts-repo.ts @@ -16,7 +16,7 @@ export async function insertUiArtifact( .prepare( `INSERT INTO ui_artifacts ( id, user_id, title, description, source_code, source_type, - parameters, include_in_search_results, created_at, updated_at + parameters, hidden, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, ) .bind( @@ -27,7 +27,7 @@ export async function insertUiArtifact( row.code, row.runtime, row.parameters ?? null, - row.include_in_search_results ? 1 : 0, + row.hidden ? 1 : 0, row.created_at ?? now, row.updated_at ?? now, ) @@ -42,7 +42,7 @@ export async function getUiArtifactById( const result = await db .prepare( `SELECT id, user_id, title, description, source_code, source_type, - parameters, include_in_search_results, created_at, updated_at + parameters, hidden, created_at, updated_at FROM ui_artifacts WHERE id = ? AND user_id = ?`, ) .bind(artifactId, userId) @@ -63,7 +63,7 @@ export async function getUiArtifactByOwnerIds( const result = await db .prepare( `SELECT id, user_id, title, description, source_code, source_type, - parameters, include_in_search_results, created_at, updated_at + parameters, hidden, created_at, updated_at FROM ui_artifacts WHERE id = ? AND user_id IN (${placeholders}) LIMIT 1`, @@ -98,7 +98,7 @@ export async function updateUiArtifact( | 'code' | 'runtime' | 'parameters' - | 'include_in_search_results' + | 'hidden' > >, ): Promise { @@ -124,11 +124,8 @@ export async function updateUiArtifact( if (updates.parameters !== undefined) { addAssignment('parameters', updates.parameters ?? null) } - if (updates.include_in_search_results !== undefined) { - addAssignment( - 'include_in_search_results', - updates.include_in_search_results ? 1 : 0, - ) + if (updates.hidden !== undefined) { + addAssignment('hidden', updates.hidden ? 1 : 0) } addAssignment('updated_at', new Date().toISOString()) @@ -145,22 +142,20 @@ export async function updateUiArtifact( export async function listUiArtifactsByUserId( db: D1Database, userId: string, - options?: { includeInSearchResults?: boolean }, + options?: { hidden?: boolean }, ): Promise> { - const includeInSearchResults = options?.includeInSearchResults + const hidden = options?.hidden const { results } = await db .prepare( `SELECT id, user_id, title, description, source_code, source_type, - parameters, include_in_search_results, created_at, updated_at + parameters, hidden, created_at, updated_at FROM ui_artifacts WHERE user_id = ? - ${includeInSearchResults === undefined ? '' : 'AND include_in_search_results = ?'}`, + ${hidden === undefined ? '' : 'AND hidden = ?'}`, ) .bind( userId, - ...(includeInSearchResults === undefined - ? [] - : [includeInSearchResults ? 1 : 0]), + ...(hidden === undefined ? [] : [hidden ? 1 : 0]), ) .all>() return (results ?? []).map(mapRow) @@ -175,10 +170,10 @@ function mapRow(row: Record): UiArtifactRow { code: String(row['source_code']), runtime: String(row['source_type']) as UiArtifactRow['runtime'], parameters: row['parameters'] == null ? null : String(row['parameters']), - include_in_search_results: - row['include_in_search_results'] === 1 || - row['include_in_search_results'] === '1' || - row['include_in_search_results'] === true, + hidden: + row['hidden'] === 1 || + row['hidden'] === '1' || + row['hidden'] === true, created_at: String(row['created_at']), updated_at: String(row['updated_at']), } diff --git a/packages/worker/src/mcp/ui-artifacts-types.ts b/packages/worker/src/mcp/ui-artifacts-types.ts index e6462ae7b3..69c465c580 100644 --- a/packages/worker/src/mcp/ui-artifacts-types.ts +++ b/packages/worker/src/mcp/ui-artifacts-types.ts @@ -8,7 +8,7 @@ export type UiArtifactRow = { code: string runtime: UiArtifactRuntime parameters: string | null - include_in_search_results: boolean + hidden: boolean created_at: string updated_at: string } From ddd3501c20f7a2d3f8ae3786ee1adbc751fe49ab Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 31 Mar 2026 05:57:41 +0000 Subject: [PATCH 3/4] Guard vector delete on update Co-authored-by: Kent C. Dodds --- packages/worker/src/mcp/capabilities/apps/ui-save-app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/worker/src/mcp/capabilities/apps/ui-save-app.ts b/packages/worker/src/mcp/capabilities/apps/ui-save-app.ts index b483f96da6..243f50e3f4 100644 --- a/packages/worker/src/mcp/capabilities/apps/ui-save-app.ts +++ b/packages/worker/src/mcp/capabilities/apps/ui-save-app.ts @@ -135,7 +135,7 @@ export const uiSaveAppCapability = defineDomainCapability( parameters, }), }) - } else { + } else if (isUpdate) { await deleteUiArtifactVector(ctx.env, appId) } } catch (cause) { From 6a6fb30a1499a8154e643efc758d0cca49bf9e6d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 31 Mar 2026 06:04:16 +0000 Subject: [PATCH 4/4] Fix ui_save_app hidden updates Co-authored-by: Kent C. Dodds --- .../src/mcp/capabilities/apps/ui-save-app.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/worker/src/mcp/capabilities/apps/ui-save-app.ts b/packages/worker/src/mcp/capabilities/apps/ui-save-app.ts index 243f50e3f4..dd9e8973cf 100644 --- a/packages/worker/src/mcp/capabilities/apps/ui-save-app.ts +++ b/packages/worker/src/mcp/capabilities/apps/ui-save-app.ts @@ -6,6 +6,7 @@ import { type CapabilityContext } from '#mcp/capabilities/types.ts' import { errorFields, logMcpEvent } from '#mcp/observability.ts' import { deleteUiArtifact, + getUiArtifactById, insertUiArtifact, updateUiArtifact, } from '#mcp/ui-artifacts-repo.ts' @@ -84,10 +85,10 @@ export const uiSaveAppCapability = defineDomainCapability( const isUpdate = args.app_id !== undefined const appId = args.app_id ?? crypto.randomUUID() const parameters = normalizeUiArtifactParameters(args.parameters) - const hidden = args.hidden ?? true const serializedParameters = parameters ? JSON.stringify(parameters) : null + let hidden: boolean if (isUpdate) { const updated = await updateUiArtifact( @@ -100,13 +101,27 @@ export const uiSaveAppCapability = defineDomainCapability( code: args.code, runtime: args.runtime, parameters: serializedParameters, - hidden, + hidden: args.hidden, }, ) if (!updated) { throw new Error('Saved UI artifact not found for this user.') } + if (args.hidden === undefined) { + const existing = await getUiArtifactById( + ctx.env.APP_DB, + user.userId, + appId, + ) + if (!existing) { + throw new Error('Saved UI artifact not found for this user.') + } + hidden = existing.hidden + } else { + hidden = args.hidden + } } else { + hidden = args.hidden ?? true const now = new Date().toISOString() await insertUiArtifact(ctx.env.APP_DB, { id: appId,