Skip to content

feat(mcp): editable site profile — get_site_profile / set_site_profile tools - #54

Merged
Tatenda merged 2 commits into
mainfrom
develop
Jul 11, 2026
Merged

feat(mcp): editable site profile — get_site_profile / set_site_profile tools#54
Tatenda merged 2 commits into
mainfrom
develop

Conversation

@Tatenda

@Tatenda Tatenda commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

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.

  • SiteProfile singleton model + site_profile migration (auto-applied by the build's migrate-deploy step)
  • layout.tsx: static metadata → async generateMetadata() reading the profile
  • MCP v2 tools get_site_profile (articles:read) / set_site_profile (articles:write, partial patch)

🤖 Generated with Claude Code

…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>
@vercel

vercel Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
music-next Error Error Jul 11, 2026 8:10am

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown

Review Change Stack

Summary by CodeRabbit

  • New Features
    • Added editable site profile settings for the homepage title, description, and tagline.
    • Added tools to retrieve and update the site profile.
    • Homepage and browser metadata now reflect the current site profile automatically.
    • Added fallback profile information when saved settings are unavailable.

Walkthrough

Adds 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.

Changes

Site profile

Layer / File(s) Summary
Singleton persistence and service
prisma/schema.prisma, prisma/migrations/.../migration.sql, src/lib/services/site-profile-service.ts
Defines the site_profile singleton and adds default-backed reads plus trimmed partial upserts.
MCP profile tools
src/lib/mcp/contract.ts, src/lib/mcp/tools/site-profile.ts, src/app/api/mcp/route.ts
Adds validated read/write contracts, registers the tools for contract version 2, and enforces non-empty update patches.
Dynamic page metadata
src/app/layout.tsx
Fetches the site profile asynchronously for page title and description metadata.

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
Loading

Poem

I’m a rabbit with a profile to share,
Titles and taglines now bloom in the air.
Through Prisma they hop,
MCP makes them pop,
And metadata follows with care.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly reflects the new MCP site profile tools and editable site profile functionality.
Description check ✅ Passed The description matches the changeset by describing the migration, metadata update, and MCP tools.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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 develop

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

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

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.

❤️ Share

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@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: 3

🧹 Nitpick comments (2)
src/lib/services/site-profile-service.ts (1)

61-84: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

updateSiteProfile writes all fields, not just patched ones.

The update: next clause 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. Restricting update to 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 win

Consider caching the site-profile lookup to avoid a DB query per request.

generateMetadata runs on every request for this layout, and getSiteProfile issues an uncached Prisma findUnique each time. For a homepage — typically the highest-traffic route — this adds a database round-trip to every page load. Wrapping the lookup in unstable_cache (or using revalidateTag/revalidatePath when 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

📥 Commits

Reviewing files that changed from the base of the PR and between 0e5cd8f and 817ef6c.

📒 Files selected for processing (7)
  • prisma/migrations/20260710000000_add_site_profile/migration.sql
  • prisma/schema.prisma
  • src/app/api/mcp/route.ts
  • src/app/layout.tsx
  • src/lib/mcp/contract.ts
  • src/lib/mcp/tools/site-profile.ts
  • src/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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ 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 -20

Repository: 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`\("([^"]+)"\)' prisma

Repository: 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`\("([^"]+)"\)' prisma

Repository: 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 -n

Repository: 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.

Comment thread src/lib/mcp/contract.ts
Comment on lines +947 to +949
title: z.string().min(1).max(200).optional(),
description: z.string().min(1).max(500).optional(),
tagline: z.string().max(200).optional(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 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=ts

Repository: 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));
}
JS

Repository: 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:


🏁 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 ts

Repository: 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.

Comment on lines +68 to +70
title: patch.title?.trim() || current.title,
description: patch.description?.trim() || current.description,
tagline: patch.tagline?.trim() ?? current.tagline,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 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.

@Tatenda
Tatenda merged commit 1d9b119 into main Jul 11, 2026
6 of 7 checks passed
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.

1 participant