Repository navigation
Consolidate ui_save_app into a single upsert capability - #95
Conversation
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
|
Warning Rate limit exceeded
Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 1 minutes and 6 seconds. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughRemoved the separate Changes
Sequence Diagram(s)sequenceDiagram
participant Client as Client
participant MCP as MCP Handler (ui_save_app)
participant DB as APP_DB
participant AI as AI (embed/params)
participant Index as CAPABILITY_VECTOR_INDEX
participant Obs as Observability
rect rgba(200,230,201,0.5)
Client->>MCP: ui_save_app(payload { optional app_id })
end
alt app_id absent (create)
MCP->>AI: normalize & embed parameters
AI-->>MCP: embedding
MCP->>DB: insertUiArtifact(serializedParameters,...)
DB-->>MCP: inserted meta (id)
MCP->>Index: upsert(vector)
Index-->>MCP: success
MCP->>Obs: emit success event
MCP-->>Client: success (new app_id)
else app_id present (in-place update)
MCP->>DB: fetch artifact for user+app_id
DB-->>MCP: artifact found
MCP->>AI: normalize & embed parameters
AI-->>MCP: embedding
MCP->>DB: updateUiArtifact(partialUpdates)
DB-->>MCP: update meta (changes)
MCP->>Index: upsert(vector)
alt vector upsert fails
Index--x MCP: error
MCP->>Obs: emit failure event (failurePhase: handler, isUpdate: true)
MCP->>Obs: emit success event (handler completed)
MCP-->>Client: success (app_id) %% update not rolled back
else vector upsert succeeds
Index-->>MCP: success
MCP->>Obs: emit success event
MCP-->>Client: success (app_id)
end
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
|
🔎 Preview deployed: https://kody-pr-95.kentcdodds.workers.dev Worker: Mocks:
|
There was a problem hiding this comment.
🧹 Nitpick comments (1)
packages/worker/src/mcp/capabilities/apps/ui-save-app.ts (1)
116-123: Consider logging vector refresh failures for observability.When an in-place update succeeds but vector refresh fails, the error is silently swallowed. While the rationale (not failing a successful DB update) is sound, this could lead to search index drift without any visibility.
🔧 Proposed fix to add observability
} catch (cause) { if (!isUpdate) { await deleteUiArtifact(ctx.env.APP_DB, user.userId, appId) throw cause } - // A vector refresh should not fail an in-place app update. + // A vector refresh should not fail an in-place app update. + console.error('Failed to refresh vector index for app update', { + appId, + userId: user.userId, + error: cause instanceof Error ? cause.message : String(cause), + }) }🤖 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-save-app.ts` around lines 116 - 123, The catch block currently swallows vector-refresh errors for in-place updates; change it so when isUpdate is true you log the failure (instead of throwing) with contextual fields (user.userId, appId, and isUpdate) and the error/cause so we have observability into index drift. Locate the catch in saveUiApp/saveUiArtifact flow (symbols: deleteUiArtifact, ctx.env.APP_DB, user.userId, appId, isUpdate) and add a single log call to the existing logger (e.g., processLogger.error or ctx.logger.error) that includes a descriptive message and the caught cause, then continue to allow the DB update to succeed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@packages/worker/src/mcp/capabilities/apps/ui-save-app.ts`:
- Around line 116-123: The catch block currently swallows vector-refresh errors
for in-place updates; change it so when isUpdate is true you log the failure
(instead of throwing) with contextual fields (user.userId, appId, and isUpdate)
and the error/cause so we have observability into index drift. Locate the catch
in saveUiApp/saveUiArtifact flow (symbols: deleteUiArtifact, ctx.env.APP_DB,
user.userId, appId, isUpdate) and add a single log call to the existing logger
(e.g., processLogger.error or ctx.logger.error) that includes a descriptive
message and the caught cause, then continue to allow the DB update to succeed.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 8ae526be-f233-42bc-99a2-3def0062cb21
📒 Files selected for processing (5)
packages/worker/src/mcp/capabilities/apps/domain.tspackages/worker/src/mcp/capabilities/apps/index.tspackages/worker/src/mcp/capabilities/apps/ui-save-app.tspackages/worker/src/mcp/capabilities/apps/ui-update-app.tspackages/worker/src/mcp/mcp-server.mcp-e2e.test.ts
💤 Files with no reviewable changes (3)
- packages/worker/src/mcp/capabilities/apps/index.ts
- packages/worker/src/mcp/capabilities/apps/ui-update-app.ts
- packages/worker/src/mcp/capabilities/apps/domain.ts
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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/observability.workers.test.ts`:
- Around line 250-256: The test should assert the actual emitted payload shape:
check that driftEvent.errorMessage equals the raw cause string ("vector refresh
failed") rather than the rewritten message, and move the capabilityName, userId,
appId and isUpdate assertions to the nested context object (e.g., assert
driftEvent.context.capabilityName, driftEvent.context.userId,
driftEvent.context.appId, driftEvent.context.isUpdate). Update the expectations
that reference driftEvent to read from driftEvent.context and adjust the
errorMessage expectation to match the raw cause value.
🪄 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: afe2acc1-e965-40cb-a8d7-a06cd8ab5ded
📒 Files selected for processing (3)
packages/worker/src/mcp/capabilities/apps/ui-save-app.tspackages/worker/src/mcp/observability.tspackages/worker/src/mcp/observability.workers.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/worker/src/mcp/capabilities/apps/ui-save-app.ts
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Contextual fields not wrapped in
contextobject- Wrapped the user/app metadata in the
contextfield so observability payloads match the expected structure.
- Wrapped the user/app metadata in the
- ✅ Fixed:
errorFieldsextracts from wrapper instead of cause- Extracted error fields from the caught cause to preserve the original failure message.
Preview (db7cc8d60d)
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,7 +5,6 @@
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,
@@ -17,7 +16,6 @@
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,6 +6,5 @@
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-save-app.ts b/packages/worker/src/mcp/capabilities/apps/ui-save-app.ts
--- a/packages/worker/src/mcp/capabilities/apps/ui-save-app.ts
+++ b/packages/worker/src/mcp/capabilities/apps/ui-save-app.ts
@@ -3,7 +3,12 @@
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 { deleteUiArtifact, insertUiArtifact } from '#mcp/ui-artifacts-repo.ts'
+import { errorFields, logMcpEvent } from '#mcp/observability.ts'
+import {
+ deleteUiArtifact,
+ insertUiArtifact,
+ updateUiArtifact,
+} from '#mcp/ui-artifacts-repo.ts'
import { buildUiArtifactEmbedText } from '#mcp/ui-artifacts-embed.ts'
import { upsertUiArtifactVector } from '#mcp/ui-artifacts-vectorize.ts'
import { requireMcpUser } from '#mcp/capabilities/meta/require-user.ts'
@@ -13,6 +18,13 @@
} from '#mcp/ui-artifact-parameters.ts'
const inputSchema = z.object({
+ app_id: z
+ .string()
+ .min(1)
+ .optional()
+ .describe(
+ 'Optional saved UI artifact id to update in place. Omit to create a new saved app.',
+ ),
title: z.string().min(1).describe('Short title for the saved UI artifact.'),
description: z
.string()
@@ -50,8 +62,8 @@
{
name: 'ui_save_app',
description:
- 'Save a generated UI artifact for the signed-in user so it can be reopened later by app_id without sending the source back through the model context.',
- keywords: ['ui', 'app', 'artifact', 'save', 'persist', 'mcp app'],
+ 'Create or replace a saved UI artifact for the signed-in user so it can be reopened later by app_id without sending the source back through the model context.',
+ keywords: ['ui', 'app', 'artifact', 'save', 'persist', 'update', 'mcp app'],
readOnly: false,
idempotent: false,
destructive: false,
@@ -59,21 +71,44 @@
outputSchema,
async handler(args, ctx: CapabilityContext) {
const user = requireMcpUser(ctx.callerContext)
- const appId = crypto.randomUUID()
- const now = new Date().toISOString()
+ const isUpdate = args.app_id !== undefined
+ const appId = args.app_id ?? crypto.randomUUID()
const parameters = normalizeUiArtifactParameters(args.parameters)
- await insertUiArtifact(ctx.env.APP_DB, {
- id: appId,
- user_id: user.userId,
- title: args.title,
- description: args.description,
- code: args.code,
- runtime: args.runtime,
- parameters: parameters ? JSON.stringify(parameters) : null,
- created_at: now,
- updated_at: now,
- })
+ const serializedParameters = parameters
+ ? JSON.stringify(parameters)
+ : null
+ if (isUpdate) {
+ const updated = await updateUiArtifact(
+ ctx.env.APP_DB,
+ user.userId,
+ appId,
+ {
+ title: args.title,
+ description: args.description,
+ code: args.code,
+ runtime: args.runtime,
+ parameters: serializedParameters,
+ },
+ )
+ if (!updated) {
+ throw new Error('Saved UI artifact not found for this user.')
+ }
+ } else {
+ const now = new Date().toISOString()
+ await insertUiArtifact(ctx.env.APP_DB, {
+ id: appId,
+ user_id: user.userId,
+ title: args.title,
+ description: args.description,
+ code: args.code,
+ runtime: args.runtime,
+ parameters: serializedParameters,
+ created_at: now,
+ updated_at: now,
+ })
+ }
+
try {
await upsertUiArtifactVector(ctx.env, {
appId,
@@ -87,8 +122,33 @@
}),
})
} catch (cause) {
- await deleteUiArtifact(ctx.env.APP_DB, user.userId, appId)
- throw cause
+ if (!isUpdate) {
+ await deleteUiArtifact(ctx.env.APP_DB, user.userId, appId)
+ throw cause
+ }
+
+ const { errorName, errorMessage } = errorFields(cause)
+ logMcpEvent({
+ category: 'mcp',
+ tool: 'capability',
+ capabilityName: 'ui_save_app',
+ domain: capabilityDomainNames.apps,
+ outcome: 'failure',
+ durationMs: 0,
+ baseUrl: ctx.callerContext.baseUrl,
+ hasUser: true,
+ failurePhase: 'handler',
+ message:
+ 'Failed to refresh saved app vector index after in-place update.',
+ errorName,
+ errorMessage,
+ cause,
+ context: {
+ userId: user.userId,
+ appId,
+ isUpdate,
+ },
+ })
}
return {
diff --git a/packages/worker/src/mcp/capabilities/apps/ui-update-app.ts b/packages/worker/src/mcp/capabilities/apps/ui-update-app.ts
deleted file mode 100644
--- a/packages/worker/src/mcp/capabilities/apps/ui-update-app.ts
+++ /dev/null
@@ -1,147 +1,0 @@
-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, updateUiArtifact } from '#mcp/ui-artifacts-repo.ts'
-import { upsertUiArtifactVector } from '#mcp/ui-artifacts-vectorize.ts'
-import {
- normalizeUiArtifactParameters,
- parseUiArtifactParameters,
- uiArtifactParameterSchema,
-} from '#mcp/ui-artifact-parameters.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.'),
- 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.',
- ),
- parameters: z
- .array(uiArtifactParameterSchema)
- .optional()
- .describe(
- 'Optional parameter definitions for reusable saved apps. Resolved values are exposed at runtime on the imported `kodyWidget.params` API from `@kody/ui-utils`.',
- ),
- })
- .refine(
- (value) =>
- value.title !== undefined ||
- value.description !== undefined ||
- value.code !== undefined ||
- value.runtime !== undefined ||
- value.parameters !== 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(),
- parameters: z.array(uiArtifactParameterSchema).nullable(),
-})
-
-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.code !== undefined) {
- updates.code = args.code
- }
- if (args.runtime !== undefined) {
- updates.runtime = args.runtime
- }
- if (args.parameters !== undefined) {
- const parameters = normalizeUiArtifactParameters(args.parameters)
- updates.parameters = parameters ? JSON.stringify(parameters) : null
- }
-
- 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,
- code: refreshed.code,
- runtime: refreshed.runtime,
- parameters: parseUiArtifactParameters(refreshed.parameters),
- }),
- })
- } 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),
- parameters: parseUiArtifactParameters(refreshed.parameters),
- }
- },
- },
-)
\ No newline at end of file
diff --git a/packages/worker/src/mcp/mcp-server.mcp-e2e.test.ts b/packages/worker/src/mcp/mcp-server.mcp-e2e.test.ts
--- a/packages/worker/src/mcp/mcp-server.mcp-e2e.test.ts
+++ b/packages/worker/src/mcp/mcp-server.mcp-e2e.test.ts
@@ -589,7 +589,6 @@
'code',
])
expect(saveApp?.readOnly).toBeUndefined()
- expect(saveApp?.inputFields).toBeUndefined()
const textOutput =
(result as CallToolResult).content.find(
@@ -1840,7 +1839,7 @@
expect(apps?.some((app) => app.app_id === appId)).toBe(false)
})
-test('mcp server updates saved ui app artifacts', async () => {
+test('mcp server upserts saved ui app artifacts through ui_save_app', async () => {
await using database = await createTestDatabase()
await using server = await startDevServer(database.persistDir)
await using mcpClient = await createMcpClient(server.origin, database.user)
@@ -1869,7 +1868,7 @@
name: 'execute',
arguments: {
code: `async () =>
- await codemode.ui_update_app({
+ await codemode.ui_save_app({
app_id: ${JSON.stringify(appId)},
title: 'Updated App',
description: 'Updated description.',
@@ -1909,4 +1908,25 @@
expect(getPayload?.description).toBe('Updated description.')
expect(getPayload?.code).toBe('<main><h1>Updated</h1></main>')
expect(getPayload?.runtime).toBe('javascript')
+
+ const listResult = await mcpClient.client.callTool({
+ name: 'execute',
+ arguments: {
+ code: `async () => await codemode.ui_list_apps({})`,
+ },
+ })
+ const listStructured = (listResult as CallToolResult).structuredContent as
+ | {
+ result?: Record<string, unknown>
+ }
+ | undefined
+ const listPayload = listStructured?.result as
+ | Record<string, unknown>
+ | undefined
+ const apps = listPayload?.apps as
+ | Array<{ app_id?: string; title?: string }>
+ | undefined
+ expect(apps).toHaveLength(1)
+ expect(apps?.[0]?.app_id).toBe(appId)
+ expect(apps?.[0]?.title).toBe('Updated App')
})
diff --git a/packages/worker/src/mcp/observability.ts b/packages/worker/src/mcp/observability.ts
--- a/packages/worker/src/mcp/observability.ts
+++ b/packages/worker/src/mcp/observability.ts
@@ -20,6 +20,8 @@
registeredCapabilityCount?: number
errorName?: string
errorMessage?: string
+ message?: string
+ context?: Record<string, unknown>
timestamp: string
}
diff --git a/packages/worker/src/mcp/observability.workers.test.ts b/packages/worker/src/mcp/observability.workers.test.ts
--- a/packages/worker/src/mcp/observability.workers.test.ts
+++ b/packages/worker/src/mcp/observability.workers.test.ts
@@ -184,3 +184,82 @@
expect(event.outcome).toBe('success')
expect(event.failurePhase).toBeUndefined()
})
+
+test('ui_save_app logs vector refresh failure for in-place updates and still succeeds', async () => {
+ const originalInfo = console.info
+ const payloads: Array<string> = []
+ console.info = ((tag: unknown, json?: unknown) => {
+ if (tag === 'mcp-event' && typeof json === 'string') {
+ payloads.push(json)
+ }
+ }) as typeof console.info
+ try {
+ const handler = capabilityMap['ui_save_app'].handler
+ const result = await handler(
+ {
+ app_id: 'app-1',
+ title: 'Observed app',
+ description: 'Observation test app.',
+ code: '<main><h1>Observed app</h1></main>',
+ },
+ {
+ env: {
+ APP_DB: {
+ prepare() {
+ return {
+ bind() {
+ return {
+ run: async () => ({
+ meta: { changes: 1 },
+ }),
+ }
+ },
+ }
+ },
+ },
+ CAPABILITY_VECTOR_INDEX: {
+ upsert: async () => {
+ throw new Error('vector refresh failed')
+ },
+ },
+ AI: {
+ run: async () => ({
+ data: [Array.from({ length: 384 }, () => 0)],
+ }),
+ },
+ } as unknown as Env,
+ callerContext: createMcpCallerContext({
+ baseUrl: 'https://example.com',
+ user: {
+ userId: 'user-1',
+ email: 'user@example.com',
+ },
+ }),
+ },
+ )
+ expect((result as { app_id: string }).app_id).toBe('app-1')
+ } finally {
+ console.info = originalInfo
+ }
+
+ expect(payloads.length).toBe(2)
+ const driftEvent = JSON.parse(payloads[0]!) as Record<string, unknown>
+ const driftContext = driftEvent.context as Record<string, unknown> | undefined
+ expect(driftEvent.outcome).toBe('failure')
+ expect(driftEvent.failurePhase).toBe('handler')
+ expect(driftEvent.errorName).toBe('Error')
+ expect(driftEvent.errorMessage).toBe('vector refresh failed')
+ expect(driftEvent.message).toBe(
+ 'Failed to refresh saved app vector index after in-place update.',
+ )
+ expect(driftEvent.capabilityName).toBe('ui_save_app')
+ expect(driftContext).toEqual({
+ userId: 'user-1',
+ appId: 'app-1',
+ isUpdate: true,
+ })
+
+ const successEvent = JSON.parse(payloads[1]!) as Record<string, unknown>
+ expect(successEvent.outcome).toBe('success')
+ expect(successEvent.capabilityName).toBe('ui_save_app')
+})Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>

Summary
app_idsupport toui_save_appapp_idbelongs to the signed-in userui_update_appcapability and update MCP coverage for the upsert flowTesting
npm run test -- packages/worker/src/mcp/observability.workers.test.ts packages/worker/src/mcp/capabilities/capability-search.workers.test.tsnode tools/prepare-e2e-env.ts && npm run build:mcp-apps && npx vitest run --project mcp-e2e packages/worker/src/mcp/mcp-server.mcp-e2e.test.tsSummary by CodeRabbit
New Features
Bug Fixes
Refactor
Tests
Chores