Skip to content

Add ui_update_app capability - #64

Merged
kentcdodds merged 2 commits into
mainfrom
cursor/ui-app-update-capability-7f40
Mar 27, 2026
Merged

kentcdodds merged 2 commits into
mainfrom
cursor/ui-app-update-capability-7f40

Conversation

@kentcdodds

@kentcdodds kentcdodds commented Mar 27, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • add ui_update_app capability and update helper for UI artifacts
  • register update capability in apps domain exports
  • cover ui_update_app in MCP server e2e tests

Testing

  • npm run test:mcp
Open in Web Open in Cursor 

Summary by CodeRabbit

  • New Features

    • Users can update saved UI apps (title, description, keywords, code, runtime, search text) and receive updated hosted URLs.
  • Refactor

    • Shared parsing utility for artifact keywords centralized and reused across listing/getting flows.
    • Capability export surface extended to include the update capability.
  • Tests

    • End-to-end test added to verify save → update → fetch flow and hosted URL correctness.

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
@coderabbitai

coderabbitai Bot commented Mar 27, 2026 •

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d80f7289-b62e-4a73-a6dc-387b5dbfcd02

📥 Commits

Reviewing files that changed from the base of the PR and between cdc97c8 and 626002e.

📒 Files selected for processing (4)
  • packages/worker/src/mcp/capabilities/apps/ui-get-app.ts
  • packages/worker/src/mcp/capabilities/apps/ui-list-apps.ts
  • packages/worker/src/mcp/capabilities/apps/ui-update-app.ts
  • packages/worker/src/mcp/ui-artifacts-repo.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/worker/src/mcp/ui-artifacts-repo.ts
  • packages/worker/src/mcp/capabilities/apps/ui-update-app.ts

📝 Walkthrough

Walkthrough

This PR adds a new ui_update_app capability to the MCP apps domain to allow authenticated users to partially update saved UI artifacts; it performs validated partial updates, serializes keywords, refreshes the artifact’s vector index (best-effort), and returns artifact metadata including a hosted URL.

Changes

Cohort / File(s) Summary
Capability Registration
packages/worker/src/mcp/capabilities/apps/domain.ts, packages/worker/src/mcp/capabilities/apps/index.ts
Registered and re-exported the new uiUpdateAppCapability in the apps domain and capability index.
New Capability
packages/worker/src/mcp/capabilities/apps/ui-update-app.ts
Added ui_update_app domain capability: input/output schemas, auth, partial-update handler, keyword serialization, post-update reload, best-effort vector upsert, and hosted URL construction.
Data Access / Utilities
packages/worker/src/mcp/ui-artifacts-repo.ts
Added updateUiArtifact for conditional field updates (maps code→source_code, runtime→source_type, special handling for search_text) and parseStringArray utility.
Existing Capabilities Updated to Use Utility
packages/worker/src/mcp/capabilities/apps/ui-get-app.ts, packages/worker/src/mcp/capabilities/apps/ui-list-apps.ts
Replaced local parseStringArray implementations with the shared parseStringArray import from the repo module.
End-to-End Test
packages/worker/src/mcp/mcp-server-e2e.test.ts
Added E2E test that saves an app, updates it via ui_update_app, verifies response (app_id, runtime, hosted_url), and asserts persisted fields match updates.

Sequence Diagram

sequenceDiagram
    actor Client as Client
    participant Cap as ui_update_app<br/>Capability
    participant Repo as UI Artifacts Repo
    participant DB as D1 Database
    participant Vec as Vector Index

    Client->>Cap: ui_update_app(app_id, fields...)
    Cap->>Cap: requireMcpUser() (auth)
    Cap->>Cap: validate input (at least one field)
    Cap->>Repo: updateUiArtifact(db, userId, appId, updates)
    Repo->>DB: UPDATE ui_artifacts SET ... WHERE id = ? AND user_id = ?
    DB-->>Repo: result (rowCount)
    Repo-->>Cap: success boolean
    Cap->>Repo: getUiArtifactById(db, userId, appId)
    Repo->>DB: SELECT * FROM ui_artifacts WHERE id = ? AND user_id = ?
    DB-->>Repo: artifact row
    Repo-->>Cap: artifact
    Cap->>Cap: buildUiArtifactEmbedText(artifact)
    Cap->>Vec: upsertUiArtifactVector(artifactId, embeddings)
    Vec-->>Cap: ack (errors swallowed)
    Cap-->>Client: { app_id, runtime, hosted_url }
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰
I hopped to the repo, nibbling code with glee,
Updated an app, keywords scattered free,
Vectors refreshed, hosted links in sight,
A carrot-clicked commit—artifacts take flight! 🥕✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title 'Add ui_update_app capability' is clear, specific, and directly summarizes the primary change—introducing a new UI artifact update capability.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/ui-app-update-capability-7f40

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@kentcdodds
kentcdodds marked this pull request as ready for review March 27, 2026 18:58
@github-actions

github-actions Bot commented Mar 27, 2026 •

Copy link
Copy Markdown
Contributor

🔎 Preview deployed: https://kody-pr-64.kentcdodds.workers.dev

Worker: kody-pr-64
D1: kody-pr-64-db
KV: kody-pr-64-oauth-kv

Mocks:

@cursor cursor Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Vectorize failure after DB commit causes inconsistent state
    • Wrapped the vector upsert in a try/catch so update requests succeed even if vectorization fails, matching the delete handler pattern.
  • ✅ Fixed: Identical parseStringArray duplicated across three capability files
    • Extracted parseStringArray into ui-artifacts-repo.ts and imported it from the three capability files to remove duplication.
Preview (626002e618)
diff --git a/packages/worker/src/mcp/capabilities/apps/domain.ts b/packages/worker/src/mcp/capabilities/apps/domain.ts
--- a/packages/worker/src/mcp/capabilities/apps/domain.ts
+++ b/packages/worker/src/mcp/capabilities/apps/domain.ts
@@ -5,6 +5,7 @@
 import { uiListAppsCapability } from './ui-list-apps.ts'
 import { uiLoadAppSourceCapability } from './ui-load-app-source.ts'
 import { uiSaveAppCapability } from './ui-save-app.ts'
+import { uiUpdateAppCapability } from './ui-update-app.ts'
 
 export const appsDomain = defineDomain({
 	name: capabilityDomainNames.apps,
@@ -16,6 +17,7 @@
 		uiGetAppCapability,
 		uiListAppsCapability,
 		uiLoadAppSourceCapability,
+		uiUpdateAppCapability,
 		uiDeleteAppCapability,
 	],
 })

diff --git a/packages/worker/src/mcp/capabilities/apps/index.ts b/packages/worker/src/mcp/capabilities/apps/index.ts
--- a/packages/worker/src/mcp/capabilities/apps/index.ts
+++ b/packages/worker/src/mcp/capabilities/apps/index.ts
@@ -6,5 +6,6 @@
 export { uiListAppsCapability } from './ui-list-apps.ts'
 export { uiLoadAppSourceCapability } from './ui-load-app-source.ts'
 export { uiSaveAppCapability } from './ui-save-app.ts'
+export { uiUpdateAppCapability } from './ui-update-app.ts'
 
 export const appsCapabilities = appsDomain.capabilities

diff --git a/packages/worker/src/mcp/capabilities/apps/ui-get-app.ts b/packages/worker/src/mcp/capabilities/apps/ui-get-app.ts
--- a/packages/worker/src/mcp/capabilities/apps/ui-get-app.ts
+++ b/packages/worker/src/mcp/capabilities/apps/ui-get-app.ts
@@ -2,19 +2,9 @@
 import { defineDomainCapability } from '#mcp/capabilities/define-domain-capability.ts'
 import { capabilityDomainNames } from '#mcp/capabilities/domain-metadata.ts'
 import { type CapabilityContext } from '#mcp/capabilities/types.ts'
-import { getUiArtifactById } from '#mcp/ui-artifacts-repo.ts'
+import { getUiArtifactById, parseStringArray } from '#mcp/ui-artifacts-repo.ts'
 import { requireMcpUser } from '#mcp/capabilities/meta/require-user.ts'
 
-function parseStringArray(raw: string): Array<string> {
-	try {
-		const value = JSON.parse(raw) as unknown
-		if (!Array.isArray(value)) return []
-		return value.filter((entry): entry is string => typeof entry === 'string')
-	} catch {
-		return []
-	}
-}
-
 const outputSchema = z.object({
 	app_id: z.string(),
 	title: z.string(),

diff --git a/packages/worker/src/mcp/capabilities/apps/ui-list-apps.ts b/packages/worker/src/mcp/capabilities/apps/ui-list-apps.ts
--- a/packages/worker/src/mcp/capabilities/apps/ui-list-apps.ts
+++ b/packages/worker/src/mcp/capabilities/apps/ui-list-apps.ts
@@ -2,7 +2,7 @@
 import { defineDomainCapability } from '#mcp/capabilities/define-domain-capability.ts'
 import { capabilityDomainNames } from '#mcp/capabilities/domain-metadata.ts'
 import { type CapabilityContext } from '#mcp/capabilities/types.ts'
-import { listUiArtifactsByUserId } from '#mcp/ui-artifacts-repo.ts'
+import { listUiArtifactsByUserId, parseStringArray } from '#mcp/ui-artifacts-repo.ts'
 import { requireMcpUser } from '#mcp/capabilities/meta/require-user.ts'
 
 const outputSchema = z.object({
@@ -19,16 +19,6 @@
 	),
 })
 
-function parseStringArray(raw: string): Array<string> {
-	try {
-		const value = JSON.parse(raw) as unknown
-		if (!Array.isArray(value)) return []
-		return value.filter((entry): entry is string => typeof entry === 'string')
-	} catch {
-		return []
-	}
-}
-
 export const uiListAppsCapability = defineDomainCapability(
 	capabilityDomainNames.apps,
 	{

diff --git a/packages/worker/src/mcp/capabilities/apps/ui-update-app.ts b/packages/worker/src/mcp/capabilities/apps/ui-update-app.ts
new file mode 100644
--- /dev/null
+++ b/packages/worker/src/mcp/capabilities/apps/ui-update-app.ts
@@ -1,0 +1,151 @@
+import { z } from 'zod'
+import { buildSavedUiUrl } from '#worker/ui-artifact-urls.ts'
+import { defineDomainCapability } from '#mcp/capabilities/define-domain-capability.ts'
+import { capabilityDomainNames } from '#mcp/capabilities/domain-metadata.ts'
+import { type CapabilityContext } from '#mcp/capabilities/types.ts'
+import { requireMcpUser } from '#mcp/capabilities/meta/require-user.ts'
+import { buildUiArtifactEmbedText } from '#mcp/ui-artifacts-embed.ts'
+import {
+	getUiArtifactById,
+	parseStringArray,
+	updateUiArtifact,
+} from '#mcp/ui-artifacts-repo.ts'
+import { upsertUiArtifactVector } from '#mcp/ui-artifacts-vectorize.ts'
+
+const inputSchema = z
+	.object({
+		app_id: z
+			.string()
+			.min(1)
+			.describe('Saved UI artifact id returned by ui_save_app.'),
+		title: z
+			.string()
+			.min(1)
+			.optional()
+			.describe('Short title for the saved UI artifact.'),
+		description: z
+			.string()
+			.min(1)
+			.optional()
+			.describe('What the saved app does and when it is useful.'),
+		keywords: z
+			.array(z.string())
+			.optional()
+			.describe('Extra search keywords for discovery in the search tool.'),
+		code: z
+			.string()
+			.min(1)
+			.optional()
+			.describe(
+				'App source for the generic MCP UI shell. Prefer a self-contained HTML document or fragment so the generated app owns the visible UI. Legacy `javascript` source is still supported for previously saved apps.',
+			),
+		runtime: z
+			.enum(['html', 'javascript'])
+			.optional()
+			.describe(
+				'Source format accepted by the generic UI shell. Prefer `html`; `javascript` is kept for legacy saved apps.',
+			),
+		search_text: z
+			.string()
+			.optional()
+			.describe(
+				'Optional retrieval-only text that improves search recall without being part of the visible app description.',
+			),
+	})
+	.refine(
+		(value) =>
+			value.title !== undefined ||
+			value.description !== undefined ||
+			value.keywords !== undefined ||
+			value.code !== undefined ||
+			value.runtime !== undefined ||
+			value.search_text !== undefined,
+		{
+			message: 'Provide at least one field to update.',
+		},
+	)
+
+const outputSchema = z.object({
+	app_id: z.string(),
+	runtime: z.enum(['html', 'javascript']),
+	hosted_url: z.string().url(),
+})
+
+export const uiUpdateAppCapability = defineDomainCapability(
+	capabilityDomainNames.apps,
+	{
+		name: 'ui_update_app',
+		description:
+			'Update an existing saved UI artifact owned by the signed-in user. Only the fields provided will be changed.',
+		keywords: ['ui', 'app', 'artifact', 'update', 'edit', 'modify'],
+		readOnly: false,
+		idempotent: false,
+		destructive: false,
+		inputSchema,
+		outputSchema,
+		async handler(args, ctx: CapabilityContext) {
+			const user = requireMcpUser(ctx.callerContext)
+			const updates: Parameters<typeof updateUiArtifact>[3] = {}
+
+			if (args.title !== undefined) {
+				updates.title = args.title
+			}
+			if (args.description !== undefined) {
+				updates.description = args.description
+			}
+			if (args.keywords !== undefined) {
+				updates.keywords = JSON.stringify(args.keywords)
+			}
+			if (args.code !== undefined) {
+				updates.code = args.code
+			}
+			if (args.runtime !== undefined) {
+				updates.runtime = args.runtime
+			}
+			if (args.search_text !== undefined) {
+				updates.search_text = args.search_text
+			}
+
+			const updated = await updateUiArtifact(
+				ctx.env.APP_DB,
+				user.userId,
+				args.app_id,
+				updates,
+			)
+			if (!updated) {
+				throw new Error('Saved UI artifact not found for this user.')
+			}
+
+			const refreshed = await getUiArtifactById(
+				ctx.env.APP_DB,
+				user.userId,
+				args.app_id,
+			)
+			if (!refreshed) {
+				throw new Error('Saved UI artifact not found after update.')
+			}
+
+			try {
+				await upsertUiArtifactVector(ctx.env, {
+					appId: refreshed.id,
+					userId: user.userId,
+					embedText: buildUiArtifactEmbedText({
+						title: refreshed.title,
+						description: refreshed.description,
+						keywords: parseStringArray(refreshed.keywords),
+						searchText: refreshed.search_text,
+						runtime: refreshed.runtime,
+					}),
+				})
+			} catch {
+				// Vector refresh should not fail the primary update.
+			}
+
+			return {
+				app_id: refreshed.id,
+				runtime: refreshed.runtime,
+				hosted_url: buildSavedUiUrl(ctx.callerContext.baseUrl, refreshed.id),
+			}
+		},
+	},
+)

diff --git a/packages/worker/src/mcp/mcp-server-e2e.test.ts b/packages/worker/src/mcp/mcp-server-e2e.test.ts
--- a/packages/worker/src/mcp/mcp-server-e2e.test.ts
+++ b/packages/worker/src/mcp/mcp-server-e2e.test.ts
@@ -1445,3 +1445,79 @@
 	const apps = listPayload?.apps as Array<{ app_id?: string }> | undefined
 	expect(apps?.some((app) => app.app_id === appId)).toBe(false)
 })
+
+test('mcp server updates saved ui app artifacts', async () => {
+	await using database = await createTestDatabase()
+	await using server = await startDevServer(database.persistDir)
+	await using mcpClient = await createMcpClient(server.origin, database.user)
+
+	const saveResult = await mcpClient.client.callTool({
+		name: 'execute',
+		arguments: {
+			code: `async () =>
+					await codemode.ui_save_app({
+						title: 'Original App',
+						description: 'Original app description.',
+						keywords: ['original', 'ui'],
+						code: '<main><h1>Original</h1></main>',
+					})`,
+		},
+	})
+	const saveStructured = (saveResult as CallToolResult).structuredContent as
+		| {
+				result?: Record<string, unknown>
+		  }
+		| undefined
+	const savedApp = saveStructured?.result as Record<string, unknown> | undefined
+	const appId = typeof savedApp?.app_id === 'string' ? savedApp.app_id : null
+	expect(appId).not.toBeNull()
+
+	const updateResult = await mcpClient.client.callTool({
+		name: 'execute',
+		arguments: {
+			code: `async () =>
+					await codemode.ui_update_app({
+						app_id: ${JSON.stringify(appId)},
+						title: 'Updated App',
+						description: 'Updated description.',
+						keywords: ['updated', 'ui'],
+						code: '<main><h1>Updated</h1></main>',
+						runtime: 'javascript',
+						search_text: 'updated searchable text',
+					})`,
+		},
+	})
+	const updateStructured = (updateResult as CallToolResult).structuredContent as
+		| {
+				result?: Record<string, unknown>
+		  }
+		| undefined
+	const updatePayload = updateStructured?.result as
+		| Record<string, unknown>
+		| undefined
+	expect(updatePayload?.app_id).toBe(appId)
+	expect(updatePayload?.runtime).toBe('javascript')
+	expect(updatePayload?.hosted_url).toBe(`${server.origin}/ui/${appId}`)
+
+	const getResult = await mcpClient.client.callTool({
+		name: 'execute',
+		arguments: {
+			code: `async () =>
+					await codemode.ui_get_app({ app_id: ${JSON.stringify(appId)} })`,
+		},
+	})
+	const getStructured = (getResult as CallToolResult).structuredContent as
+		| {
+				result?: Record<string, unknown>
+		  }
+		| undefined
+	const getPayload = getStructured?.result as
+		| Record<string, unknown>
+		| undefined
+	expect(getPayload?.title).toBe('Updated App')
+	expect(getPayload?.description).toBe('Updated description.')
+	expect(getPayload?.keywords).toEqual(['updated', 'ui'])
+	expect(getPayload?.code).toBe('<main><h1>Updated</h1></main>')
+	expect(getPayload?.runtime).toBe('javascript')
+	expect(getPayload?.search_text).toBe('updated searchable text')
+})

diff --git a/packages/worker/src/mcp/ui-artifacts-repo.ts b/packages/worker/src/mcp/ui-artifacts-repo.ts
--- a/packages/worker/src/mcp/ui-artifacts-repo.ts
+++ b/packages/worker/src/mcp/ui-artifacts-repo.ts
@@ -1,5 +1,15 @@
 import { type UiArtifactRow } from './ui-artifacts-types.ts'
 
+export function parseStringArray(raw: string): Array<string> {
+	try {
+		const value = JSON.parse(raw) as unknown
+		if (!Array.isArray(value)) return []
+		return value.filter((entry): entry is string => typeof entry === 'string')
+	} catch {
+		return []
+	}
+}
+
 export function uiArtifactVectorId(artifactId: string): string {
 	return `ui_artifact_${artifactId}`
 }
@@ -86,6 +96,54 @@
 	return (out.meta.changes ?? 0) > 0
 }
 
+export async function updateUiArtifact(
+	db: D1Database,
+	userId: string,
+	artifactId: string,
+	updates: Partial<
+		Pick<
+			UiArtifactRow,
+			'title' | 'description' | 'keywords' | 'code' | 'runtime' | 'search_text'
+		>
+	>,
+): Promise<boolean> {
+	const assignments: Array<string> = []
+	const values: Array<unknown> = []
+	const addAssignment = (column: string, value: unknown) => {
+		assignments.push(`${column} = ?`)
+		values.push(value)
+	}
+
+	if (updates.title !== undefined) {
+		addAssignment('title', updates.title)
+	}
+	if (updates.description !== undefined) {
+		addAssignment('description', updates.description)
+	}
+	if (updates.keywords !== undefined) {
+		addAssignment('keywords', updates.keywords)
+	}
+	if (updates.code !== undefined) {
+		addAssignment('source_code', updates.code)
+	}
+	if (updates.runtime !== undefined) {
+		addAssignment('source_type', updates.runtime)
+	}
+	if (updates.search_text !== undefined) {
+		addAssignment('search_text', updates.search_text ?? null)
+	}
+
+	addAssignment('updated_at', new Date().toISOString())
+
+	const out = await db
+		.prepare(
+			`UPDATE ui_artifacts SET ${assignments.join(', ')} WHERE id = ? AND user_id = ?`,
+		)
+		.bind(...values, artifactId, userId)
+		.run()
+	return (out.meta.changes ?? 0) > 0
+}
+
 export async function listUiArtifactsByUserId(
 	db: D1Database,
 	userId: string,

Comment thread packages/worker/src/mcp/capabilities/apps/ui-update-app.ts Outdated
Comment thread packages/worker/src/mcp/capabilities/apps/ui-update-app.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/worker/src/mcp/mcp-server-e2e.test.ts (1)

1449-1523: Nice happy-path coverage; consider adding partial/clear-path assertions.

Given this capability supports partial updates, it would be valuable to add one case that updates a single field and one case that clears search_text (when null support is enabled) to lock in behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/mcp/mcp-server-e2e.test.ts` around lines 1449 - 1523, Add
two focused assertions to the existing test "mcp server updates saved ui app
artifacts": after saving the app (appId) perform a partial update via
codemode.ui_update_app that only changes title (e.g., title: 'Partially
Updated') and assert via codemode.ui_get_app that only the title changed while
other fields (description, keywords, code, runtime) remain unchanged; then
perform another update call setting search_text to null via
codemode.ui_update_app (if null support is enabled) and assert via
codemode.ui_get_app that getPayload.search_text is null (and hosted_url/app_id
remain unchanged). Use the existing variables appId, updatePayload, getPayload
and the same callTool pattern to locate where to insert these extra calls and
assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/worker/src/mcp/capabilities/apps/ui-update-app.ts`:
- Around line 54-57: The schema for the update input in ui-update-app.ts
currently defines search_text as z.string().optional(), preventing callers from
explicitly clearing it to null; update the schema for the search_text field
(both occurrences around the search_text lines) to accept null as well by
changing the Zod type to include nullable (e.g.,
z.string().nullable().optional()) so the ui_update_app input allows string |
null | undefined.

---

Nitpick comments:
In `@packages/worker/src/mcp/mcp-server-e2e.test.ts`:
- Around line 1449-1523: Add two focused assertions to the existing test "mcp
server updates saved ui app artifacts": after saving the app (appId) perform a
partial update via codemode.ui_update_app that only changes title (e.g., title:
'Partially Updated') and assert via codemode.ui_get_app that only the title
changed while other fields (description, keywords, code, runtime) remain
unchanged; then perform another update call setting search_text to null via
codemode.ui_update_app (if null support is enabled) and assert via
codemode.ui_get_app that getPayload.search_text is null (and hosted_url/app_id
remain unchanged). Use the existing variables appId, updatePayload, getPayload
and the same callTool pattern to locate where to insert these extra calls and
assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d413e04a-1920-4809-b674-5340080b4f6a

📥 Commits

Reviewing files that changed from the base of the PR and between d34677e and cdc97c8.

📒 Files selected for processing (5)
  • packages/worker/src/mcp/capabilities/apps/domain.ts
  • packages/worker/src/mcp/capabilities/apps/index.ts
  • packages/worker/src/mcp/capabilities/apps/ui-update-app.ts
  • packages/worker/src/mcp/mcp-server-e2e.test.ts
  • packages/worker/src/mcp/ui-artifacts-repo.ts

Comment on lines +54 to +57
search_text: z
.string()
.optional()
.describe(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Allow explicit search_text clearing in update input.

Line 54 currently accepts only string | undefined, so callers cannot clear search_text to NULL via ui_update_app, despite repo support for null updates.

Suggested patch
-		search_text: z
-			.string()
-			.optional()
+		search_text: z
+			.string()
+			.nullable()
+			.optional()
 			.describe(
 				'Optional retrieval-only text that improves search recall without being part of the visible app description.',
 			),

Also applies to: 111-113

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/mcp/capabilities/apps/ui-update-app.ts` around lines 54 -
57, The schema for the update input in ui-update-app.ts currently defines
search_text as z.string().optional(), preventing callers from explicitly
clearing it to null; update the schema for the search_text field (both
occurrences around the search_text lines) to accept null as well by changing the
Zod type to include nullable (e.g., z.string().nullable().optional()) so the
ui_update_app input allows string | null | undefined.

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
@kentcdodds
kentcdodds merged commit 43055fb into main Mar 27, 2026
9 checks passed
@kentcdodds
kentcdodds deleted the cursor/ui-app-update-capability-7f40 branch March 27, 2026 19:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants