Conversation
…e tools Makes the site's identity (homepage title / meta description / tagline) editable so a connected AI client (the Pic-A-Site CMS) can manage it over MCP — previously these were hardcoded in layout.tsx with nothing to write to. - SiteProfile singleton model (id="default") + migration; service with the previous static values as fallback defaults (never breaks rendering, even pre-migration — the read is try/caught). - layout.tsx: static `metadata` → async `generateMetadata()` reading the profile. - MCP v2 tools get_site_profile (articles:read) / set_site_profile (articles:write, partial patch, ≥1 field) in tools/site-profile.ts, registered on the v2 server. Reuses articles:* scopes so an existing article-capable client needs no new grant. Deploy: run prisma migrate deploy (adds site_profile table) on dev + prod. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Summary by CodeRabbit
WalkthroughAdds a singleton site profile stored through Prisma, exposes MCP tools to read and update it, and uses the profile title and description for dynamically generated Next.js metadata. ChangesSite profile
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant MCPClient
participant MCPRoute
participant SiteProfileTools
participant SiteProfileService
participant Prisma
MCPClient->>MCPRoute: Invoke profile tool
MCPRoute->>SiteProfileTools: Handle contract v2 request
SiteProfileTools->>SiteProfileService: Read or update profile
SiteProfileService->>Prisma: Read or upsert default profile
Prisma-->>SiteProfileService: Return profile data
SiteProfileService-->>MCPClient: Return profile response
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration. 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: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
src/lib/services/site-profile-service.ts (1)
61-84: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win
updateSiteProfilewrites all fields, not just patched ones.The
update: nextclause overwrites all three fields on every call, even when only one is patched. This creates a TOCTOU window: two concurrent updates to different fields can clobber each other. Restrictingupdateto only the patched fields eliminates the race and reduces unnecessary writes.♻️ Proposed refactor — partial update
export async function updateSiteProfile(patch: { title?: string; description?: string; tagline?: string; }): Promise<SiteProfile> { - const current = await getSiteProfile(); - const next = { - title: patch.title?.trim() ?? current.title, - description: patch.description?.trim() ?? current.description, - tagline: patch.tagline?.trim() ?? current.tagline, - }; + const updateData: Record<string, string> = {}; + if (patch.title !== undefined) updateData.title = patch.title.trim(); + if (patch.description !== undefined) updateData.description = patch.description.trim(); + if (patch.tagline !== undefined) updateData.tagline = patch.tagline.trim(); + + const createData = { + id: SINGLETON_ID, + title: patch.title?.trim() || SITE_PROFILE_DEFAULTS.title, + description: patch.description?.trim() || SITE_PROFILE_DEFAULTS.description, + tagline: patch.tagline?.trim() ?? SITE_PROFILE_DEFAULTS.tagline, + ...updateData, + }; + const row = await prisma.siteProfile.upsert({ where: { id: SINGLETON_ID }, - create: { id: SINGLETON_ID, ...next }, - update: next, + create: createData, + update: updateData, select: { title: true, description: true, tagline: true, updatedAt: true }, });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/lib/services/site-profile-service.ts` around lines 61 - 84, Update updateSiteProfile so Prisma’s upsert update payload contains only fields explicitly supplied in patch, while preserving the existing trimmed-value behavior and create payload. Build the update object from defined patch properties rather than using the merged next object, preventing concurrent updates to different fields from overwriting one another.src/app/layout.tsx (1)
42-80: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winConsider caching the site-profile lookup to avoid a DB query per request.
generateMetadataruns on every request for this layout, andgetSiteProfileissues an uncached PrismafindUniqueeach time. For a homepage — typically the highest-traffic route — this adds a database round-trip to every page load. Wrapping the lookup inunstable_cache(or usingrevalidateTag/revalidatePathwhen the profile is updated via MCP) would eliminate the repeated query while keeping metadata editable.This is an optimization, not a correctness issue — the current code is functionally correct and the service's catch-all ensures rendering never breaks.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/app/layout.tsx` around lines 42 - 80, Cache the site-profile lookup used by generateMetadata instead of querying Prisma on every request. Wrap getSiteProfile with the project’s established unstable_cache or equivalent cache mechanism, and ensure profile updates through the MCP setter invalidate or revalidate that cached value so metadata remains editable.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@prisma/migrations/20260710000000_add_site_profile/migration.sql`:
- Line 4: Update the site_profile.id column definition in the migration to
include the database default value "default", matching the SiteProfile.id schema
declaration and eliminating migration drift.
In `@src/lib/mcp/contract.ts`:
- Around line 947-949: Update the schema fields for title and description to
trim input before applying min(1), ensuring whitespace-only values fail
validation while preserving their existing length limits and optional behavior.
In `@src/lib/services/site-profile-service.ts`:
- Around line 68-70: Update the patch field handling in the site profile update
logic to use nullish coalescing consistently for title, description, and
tagline, then add handler-level validation in set_site_profile that rejects
title or description values whose trimmed content is empty by throwing the
established bad-request McpError.
---
Nitpick comments:
In `@src/app/layout.tsx`:
- Around line 42-80: Cache the site-profile lookup used by generateMetadata
instead of querying Prisma on every request. Wrap getSiteProfile with the
project’s established unstable_cache or equivalent cache mechanism, and ensure
profile updates through the MCP setter invalidate or revalidate that cached
value so metadata remains editable.
In `@src/lib/services/site-profile-service.ts`:
- Around line 61-84: Update updateSiteProfile so Prisma’s upsert update payload
contains only fields explicitly supplied in patch, while preserving the existing
trimmed-value behavior and create payload. Build the update object from defined
patch properties rather than using the merged next object, preventing concurrent
updates to different fields from overwriting one another.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: a6941b67-0b0b-4641-8e9e-e59a3e2c53b2
📒 Files selected for processing (7)
prisma/migrations/20260710000000_add_site_profile/migration.sqlprisma/schema.prismasrc/app/api/mcp/route.tssrc/app/layout.tsxsrc/lib/mcp/contract.tssrc/lib/mcp/tools/site-profile.tssrc/lib/services/site-profile-service.ts
| -- Editable site identity (singleton, id = 'default'). Read by generateMetadata | ||
| -- and by the get_site_profile / set_site_profile MCP tools. | ||
| CREATE TABLE "site_profile" ( | ||
| "id" TEXT NOT NULL, |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if other migrations with scalar `@default`("...") include DEFAULT in SQL
rg -n "DEFAULT '" prisma/migrations/ --include='*.sql' | head -20Repository: mmogomedia/music-next
Length of output: 601
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== schema =="
if [ -f prisma/schema.prisma ]; then
nl -ba prisma/schema.prisma | sed -n '1,260p'
else
echo "missing prisma/schema.prisma"
fi
echo
echo "== target migration =="
nl -ba prisma/migrations/20260710000000_add_site_profile/migration.sql
echo
echo "== SQL DEFAULT occurrences in prisma/migrations =="
rg -n -S "DEFAULT\s+'" prisma/migrations
echo
echo "== Prisma scalar string defaults in schema files =="
rg -n -S '`@default`\("([^"]+)"\)' prismaRepository: mmogomedia/music-next
Length of output: 212
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== prisma/schema.prisma (first 260 lines) =="
if [ -f prisma/schema.prisma ]; then
cat -n prisma/schema.prisma | sed -n '1,260p'
else
echo "missing prisma/schema.prisma"
fi
echo
echo "== prisma/migrations/20260710000000_add_site_profile/migration.sql =="
cat -n prisma/migrations/20260710000000_add_site_profile/migration.sql
echo
echo "== SQL DEFAULT occurrences in prisma/migrations =="
rg -n -S "DEFAULT '" prisma/migrations
echo
echo "== Prisma scalar string defaults in prisma =="
rg -n -S '`@default`\("([^"]+)"\)' prismaRepository: mmogomedia/music-next
Length of output: 19081
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== site_profile model in prisma/schema.prisma =="
sed -n '1750,1775p' prisma/schema.prisma | cat -nRepository: mmogomedia/music-next
Length of output: 1217
Add DEFAULT 'default' to site_profile.id
prisma/schema.prisma declares SiteProfile.id String @id @default("default"), but this migration creates the column without the database default. That leaves the migration out of sync with the schema and will keep prisma migrate diff reporting drift.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@prisma/migrations/20260710000000_add_site_profile/migration.sql` at line 4,
Update the site_profile.id column definition in the migration to include the
database default value "default", matching the SiteProfile.id schema declaration
and eliminating migration drift.
| title: z.string().min(1).max(200).optional(), | ||
| description: z.string().min(1).max(500).optional(), | ||
| tagline: z.string().max(200).optional(), |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check the Zod version in the project
rg -n '"zod"' package.json
# Check if .trim() is used elsewhere in the project with .shape
rg -nC2 '\.trim\(\)' src/lib/mcp/ --type=tsRepository: mmogomedia/music-next
Length of output: 6005
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the relevant schema and tool wiring.
sed -n '920,980p' src/lib/mcp/contract.ts
echo '---'
sed -n '1,220p' src/lib/mcp/tools/site-profile.ts
# Find all uses of inputSchema and any direct .shape access in MCP code.
echo '--- inputSchema references ---'
rg -n "inputSchema|\.shape\b" src/lib/mcp -t ts
# Inspect package metadata for the MCP SDK version.
echo '--- package.json mcp refs ---'
rg -n '"`@modelcontextprotocol/sdk`"|mcp' package.json
# Probe Zod 4 semantics in a read-only way if the dependency is installed.
echo '--- zod resolution probe ---'
node - <<'JS'
try {
const zod = require('zod');
const s = zod.object({
title: zod.string().trim().min(1).max(200).optional(),
});
const shape = s.shape;
console.log('zod_loaded', true);
console.log('schema_type', typeof s);
console.log('shape_keys', Object.keys(shape));
console.log('title_type', typeof shape.title);
console.log('title_has_parse', typeof shape.title.parse);
console.log('title_has_min', typeof shape.title.min);
console.log('title_def_type', shape.title?._def?.typeName ?? null);
} catch (e) {
console.log('zod_loaded', false);
console.log(String(e && e.message ? e.message : e));
}
JSRepository: mmogomedia/music-next
Length of output: 6617
🌐 Web query:
Model Context Protocol SDK registerTool inputSchema shape Zod v4 .shape ZodString trim min compatibility
💡 Result:
As of July 2026, the Model Context Protocol (MCP) TypeScript SDK (v2+) is library-agnostic and uses the Standard Schema specification for tool input schemas [1][2]. 1. InputSchema Compatibility: The registerTool method's inputSchema accepts any library that implements the Standard Schema interface for validation and JSON Schema conversion [1][3]. Zod v4 is fully supported and works natively without changes [1]. 2. Schema Shape: The MCP specification requires tool input schemas to describe an object with a root type of 'object' [4][3]. Consequently, you must wrap your schema using z.object({...}) [5][6]. Using raw Zod shapes (e.g., { name: z.string() }) directly in inputSchema is deprecated, though the SDK includes compatibility shims that auto-wrap them in z.object() [2][7]. 3. Zod Methods (trim, min): You can freely chain Zod string methods like .trim() and .min() within your schema definition [8]. In Zod v4, these methods are effectively preserved and chainable [8]. The resulting schema remains compatible with the MCP SDK, which will use these constraints to generate the corresponding JSON Schema for model interaction and validate incoming tool arguments at runtime [9][2]. 4. Versioning Notes: If you encounter type errors when using Zod v4 (e.g., ZodString not assignable to AnySchema), ensure your project is using a recent version of the MCP SDK (v1.29.0 or later), which was updated to handle structural compatibility with Zod v4 across different dependency resolution paths [10][11]. If issues persist due to package manager dependency duplication, forcing a resolution to the same Zod v4 version in your package manifest (e.g., via overrides) is the standard resolution [10].
Citations:
- 1: feat: support Standard Schema for tool/prompt schemas modelcontextprotocol/typescript-sdk#1689
- 2: https://ts.sdk.modelcontextprotocol.io/v2/advanced/schema-libraries
- 3: https://github.com/modelcontextprotocol/typescript-sdk/blob/7d7e62cc/packages/core/src/util/standardSchema.ts
- 4: https://github.com/modelcontextprotocol/typescript-sdk/blob/7d7e62cc/packages/core/src/types/schemas.ts
- 5: https://ts.sdk.modelcontextprotocol.io/v2/documents/Documents.Server_Guide.html
- 6: https://github.com/modelcontextprotocol/typescript-sdk/blob/main/docs/servers/tools.md
- 7: feat(compat): registerTool/registerPrompt accept raw Zod shape, auto-wrap with z.object() modelcontextprotocol/typescript-sdk#1901
- 8: Zod transformations understanding colinhacks/zod#5734
- 9: https://ts.sdk.modelcontextprotocol.io/v2/servers/tools.html
- 10:
registerTooltype error with zod 4.4.x —ZodStringnot assignable toAnySchemamodelcontextprotocol/typescript-sdk#1987 - 11: fix(server): accept structurally compatible Zod v4 schemas modelcontextprotocol/typescript-sdk#1990
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the update logic that consumes SetSiteProfileInput.
fd -a 'site-profile-service.ts' src
echo '---'
sed -n '1,260p' src/lib/services/site-profile-service.ts
# Check for trimming/fallback behavior on title/description/tagline.
echo '--- grep ---'
rg -n "title|description|tagline|trim\(|\|\|" src/lib/services/site-profile-service.ts src/lib -t tsRepository: mmogomedia/music-next
Length of output: 50378
Trim title and description before min(1)
title and description still accept whitespace-only input today, then updateSiteProfile() trims and falls back to the current value, so the caller gets no error. z.string().trim().min(1) remains compatible with the MCP SDK inputSchema, so the schema-level fix is the cleanest path here.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/lib/mcp/contract.ts` around lines 947 - 949, Update the schema fields for
title and description to trim input before applying min(1), ensuring
whitespace-only values fail validation while preserving their existing length
limits and optional behavior.
| title: patch.title?.trim() || current.title, | ||
| description: patch.description?.trim() || current.description, | ||
| tagline: patch.tagline?.trim() ?? current.tagline, |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Inconsistent || vs ?? for trimmed patch fields.
title and description use || (empty-after-trim falls back to current), while tagline uses ?? (empty-after-trim is kept). This is intentional — title/description should never be empty — but the Zod schema's .min(1) validates the raw string, not the trimmed one. So title: " " passes schema validation (length 3) yet is silently ignored by ||, returning the old title with no error. Consider using ?? consistently and enforcing non-empty-after-trim at the schema or handler level.
♻️ Proposed fix — use ?? consistently, guard in the handler
const next = {
- title: patch.title?.trim() || current.title,
- description: patch.description?.trim() || current.description,
+ title: patch.title?.trim() ?? current.title,
+ description: patch.description?.trim() ?? current.description,
tagline: patch.tagline?.trim() ?? current.tagline,
};Then add a whitespace guard in the set_site_profile handler (see src/lib/mcp/tools/site-profile.ts):
if (args.title !== undefined && args.title.trim() === '') {
throw new McpError('bad_request', 'title must not be empty or whitespace-only.', 400);
}
// same for description🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/lib/services/site-profile-service.ts` around lines 68 - 70, Update the
patch field handling in the site profile update logic to use nullish coalescing
consistently for title, description, and tagline, then add handler-level
validation in set_site_profile that rejects title or description values whose
trimmed content is empty by throwing the established bad-request McpError.
Ships the site-profile MCP tools to prod so a connected AI client (the Pic-A-Site CMS) can read and manage the site's identity (homepage title / meta description / tagline) over MCP.
site_profilemigration (auto-applied by the build's migrate-deploy step)layout.tsx: static metadata → asyncgenerateMetadata()reading the profileget_site_profile(articles:read) /set_site_profile(articles:write, partial patch)🤖 Generated with Claude Code